Comment mettre en place une application micro-frontend en Angular pour consommer l’API ?

Chaque semaine, retrouvez sur notre blog un article relatif à notre série « Guide pratique dédié à la sécurisation d’une application Java Spring Boot et son déploiement dans le Cloud AWS ». L’objectif est de réaliser, pas à pas, la configuration d’un environnement sécurisé, la création d’une API et in fine, son déploiement dans le cloud.

Introduction

Dans ce nouvel épisode, nous allons aborder la question des applications micro-frontends. Nous allons plus précisemment voir comment mettre en place une application micro-frontend en Angular pour consommer une API sécurisée.

Prérequis

Avant de plonger dans le code, assurez-vous d’avoir les bonnes versions des outils installés :

  • Node.js : v18.17.1
  • NPM : v10.4.0
  • Angular CLI : 17.3.7

L’importance de la sécurité dans une architecture micro-frontend

Nous ne le dirons jamais assez, la sécurité est primordiale dans toute application. Et cela vaut doublement pour les architectures micro-frontend. En segmentant votre application en plusieurs parties indépendantes, vous devez vous assurer que chaque segment est sécurisé pour éviter des vulnérabilités potentielles. C’est pourquoi, utiliser OAuth2 et OpenID Connect avec Keycloak pour sécuriser vos API est une excellente façon de garantir que seules les demandes authentifiées et autorisées peuvent accéder à vos ressources.

Exposition d’une nouvelle API en Spring Boot

Suite à la création de notre projet Spring Boot dans l’épisode précédent, nous poursuivons aujourd’hui le développement en mettant en œuvre une nouvelle API. Cette API sera responsable de l’exposition d’une méthode de vérification d’autorisation via Keycloak. Elle utilisera la méthode de connexion pour générer et retourner un token, à condition que les identifiants fournis soient valides.

1. Création d’une nouvelle API Spring Boot

Nous entamerons le processus par la mise en place d’une nouvelle API Spring Boot. Cette API comprendra un contrôleur qui mettra à disposition deux points de terminaison : /auth/login et /auth/logout. Vous trouverez ci-dessous, les détails des classes requises pour cette configuration.

Gestion des requêtes d’authentification : AuthController.java

Le AuthController est conçu pour traiter de manière sécurisée les données sensibles des utilisateurs. Il garantit ainsi l’intégrité et la confidentialité des informations d’identification lors des processus de connexion et de déconnexion.

Fonctionnalités principales :
  • Gestion de la connexion : le contrôleur expose un endpoint /auth/login qui accepte les requêtes POST contenant les identifiants de l’utilisateur, tels que le nom d’utilisateur et le mot de passe. Ces informations sont encapsulées dans un objet AuthRequest et transmises à la méthode login. Cette méthode vérifie d’abord l’existence des champs requis. Si les champs sont présents et valides, elle prépare et envoie une requête à Keycloak, un serveur d’identité qui gère l’authentification et la délivrance des tokens.
  • Génération du token : en cas de succès de l’authentification par Keycloak, un token est généré et renvoyé dans un objet AuthResponse. Ce token est essentiel pour les sessions utilisateur et pour sécuriser l’accès aux autres parties de l’application nécessitant une authentification.
  • Gestion des erreurs : en revanche, si les identifiants sont incorrects ou si Keycloak ne parvient pas à authentifier l’utilisateur, le contrôleur capture l’exception et renvoie une réponse appropriée. Cette dernière indique l’échec de l’authentification avec un message d’erreur détaillé.
  • Déconnexion : le contrôleur offre également un endpoint /auth/logout qui permet aux utilisateurs de se déconnecter de l’application. Cette fonctionnalité est intéressante, notamment pour la gestion de la sécurité des sessions utilisateur.
Sécurité et bonnes pratiques :
  • Validation des entrées : le contrôleur utilise des validations pour s’assurer que les données reçues via les requêtes sont valides avant de les traiter. Cela aide à prévenir les attaques telles que l’injection SQL et le cross-site scripting (XSS).
  • Gestion sécurisée des exceptions : toutes les exceptions potentielles sont gérées de manière sécurisée pour éviter la divulgation d’informations sensibles sur l’infrastructure sous-jacente de l’application.
  • Utilisation de HTTPS : il est recommandé que toutes les communications entre le client et le serveur soient sécurisées via HTTPS. Effectivement, cela assure que les données sensibles telles que les mots de passe et les tokens ne soient pas exposées à des écoutes indiscrètes.

@RestController
public class AuthController {

@Autowired
private RestTemplate restTemplate;

@PostMapping(« /auth/login »)
public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
String username = authRequest.getUsername();
String password = authRequest.getPassword();

if (username == null || password == null) {
AuthResponse authResponse = new AuthResponse();
authResponse.setStatus(« KO »);
authResponse.setErrorMessage(« Missing username or password »);
return ResponseEntity.badRequest().body(authResponse);
}

// Prepare the request body
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add(« username », username);
map.add(« password », password);
map.add(« client_id », « springboot-openid-client-app »);
map.add(« client_secret », « TlOjOmy4vEQbPsKjMqV009wYHdlGaIG2 »);
map.add(« grant_type », « password »);

try {

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(map, headers);

// Send the request to Keycloak

ResponseEntity response = restTemplate.postForEntity(« http://localhost:8180/auth/realms/Keycloak_SpringBoot/protocol/openid-connect/token », requestEntity, KeycloakResponse.class);

// If the request is successful, create AuthResponse and return
if(response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
log.info(« Authorized »);
AuthResponse authResponse = new AuthResponse();
authResponse.setUsername(username);
authResponse.setToken(response.getBody().getAccessToken());
authResponse.setStatus(« OK »);
return ResponseEntity.ok(authResponse);
} else {
log.info(« Unauthorized »);
AuthResponse authResponse = new AuthResponse();
authResponse.setStatus(« KO »);
authResponse.setErrorMessage(« Unauthorized »);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authResponse);
}

} catch (HttpClientErrorException e) {
log.error(« Error while calling Keycloak: {} », e.getResponseBodyAsString());
// If the request fails, return the status code and error message
AuthResponse authResponse = new AuthResponse();
authResponse.setStatus(« KO »);
authResponse.setErrorMessage(e.getResponseBodyAsString());
return ResponseEntity.status(e.getStatusCode()).body(authResponse);
}

}

@PostMapping(« /auth/logout »)
public ResponseEntity<?> logout() {
// Implement the logout logic here
return ResponseEntity.ok(« Logged out successfully »);
}

}

Ce qu’il faut retenir ici est que AuthController.java est essentiel pour la sécurité de l’application Spring Boot. Il gère efficacement les processus d’authentification et de déconnexion tout en assurant la sécurité des données utilisateur

Définition des classes pour les requêtes et les réponses d’authentification, structurant les données échangées lors du processus de connexion :

Ce modèle représente les demandes d’authentification. Nous discuterons de sa structure et de son importance par la suite, dans le processus de sécurisation.

import lombok.*;
import java.io.Serializable;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class AuthRequest implements Serializable {

private String username;
private String password;

}

Ici, nous décrivons comment les réponses aux demandes d’authentification sont structurées et sécurisées dans notre application.

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.*;
import java.io.Serializable;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class AuthResponse implements Serializable {

@JsonInclude(JsonInclude.Include.NON_NULL)
private String username;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String token;
private String status;
private String errorMessage;

}

Cette troisième classe joue un rôle dans la communication avec Keycloak pour l’authentification des utilisateurs.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class KeycloakRequest implements Serializable {

@JsonProperty(« username« )
private String username;
@JsonProperty(« password« )
private String password;
@JsonProperty(« client_id« )
private String clientId;
@JsonProperty(« client_secret« )
private String clientSecret;
@JsonProperty(« grant_type« )
private String grantType;
@JsonProperty(« scope« )
private String scope;

}

Cette classe gère les réponses de Keycloak. Nous verrons par la suite comment elle aide à sécuriser la communication entre l’application et le service d’authentification.

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import java.io.Serializable;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class KeycloakResponse implements Serializable {

@JsonProperty(« access_token« )
private String accessToken;
@JsonProperty(« expires_in« )
private int expiresIn;
@JsonProperty(« refresh_expires_in« )
private int refreshExpiresIn;
@JsonProperty(« refresh_token« )
private String refreshToken;
@JsonProperty(« token_type« )
private String tokenType;
@JsonProperty(« id_token« )
private String idToken;
@JsonProperty(« not-before-policy« )
private int notBeforePolicy;
@JsonProperty(« session_state« )
private String sessionState;
private String scope;
}

2. Mise à jour de la configuration de Spring Security

Dans cette seconde section, nous nous concentrerons sur le fichier SecurityConfig.java, qui joue un rôle central dans la définition des règles de sécurité au niveau de l’application. Le fichier SecurityConfig.java est le cœur de la configuration de sécurité dans une application Spring Boot. Il étend la classe WebSecurityConfigurerAdapter et fournit un modèle pratique pour personnaliser à la fois l’authentification et l’autorisation dans l’application.

@Configuration
@EnableWebSecurity
class SecurityConfig {

private static final String GROUPS = « groups »;
private static final String REALM_ACCESS_CLAIM = « realm_accessy »;
private static final String ROLES_CLAIM = « roles »;

private final KeycloakLogoutHandler keycloakLogoutHandler;

SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {

this.keycloakLogoutHandler = keycloakLogoutHandler;

}

@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {

CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
corsConfiguration.setAllowedMethods(Arrays.asList(« GET », « POST », « PUT », « DELETE », « OPTIONS »));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(« /** », corsConfiguration);

http.cors(cors -> cors.configurationSource(source))
.authorizeHttpRequests(auth -> auth
// Allows preflight requests from browser
.requestMatchers(new AntPathRequestMatcher(« /api/public* »))
.permitAll()
.requestMatchers(new AntPathRequestMatcher(« /auth/login* »))
.permitAll()
.requestMatchers(new AntPathRequestMatcher(« /auth/logout* »))
.permitAll()
.requestMatchers(new AntPathRequestMatcher(« /customers* », HttpMethod.OPTIONS.name()))
.permitAll()
.requestMatchers(new AntPathRequestMatcher(« /customers* »))
.hasRole(« user »)
.requestMatchers(new AntPathRequestMatcher(« / »))
.permitAll()
.anyRequest()
.authenticated());

http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
http.oauth2Login(oauth2 -> oauth2
.successHandler(new OAuth2LoginSuccessHandler())) // Utiliser le gestionnaire de succès personnalisé
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl(« / »));

http.csrf(AbstractHttpConfigurer::disable);
// Ajoutez l’intercepteur de sécurité
http.addFilterBefore(new SecurityInterceptor(), SecurityContextPersistenceFilter.class);

return http.build();
}

// Suite du code …

}

Renforcement de la sécurité

Pour aller plus loin et renforcer la sécurité de notre configuration, il est essentiel d’activer certaines fonctionnalités supplémentaires fournies par Spring Security :

  • CSRF (Cross-Site Request Forgery) : Spring Security active la protection CSRF par défaut. Mais il est important de s’assurer que cette fonctionnalité est bien configurée, surtout si l’application expose des API REST.
  • CORS (Cross-Origin Resource Sharing) : si l’application doit accepter des requêtes cross-origin, la configuration CORS doit être définie correctement pour éviter toute vulnérabilité.
  • HTTPS : forcer l’utilisation de HTTPS au lieu de HTTP pour toutes les communications est crucial pour la sécurité des données en transit.

Endpoints publics et privés

Ce contrôleur gère les requêtes vers les endpoints publics et privés. Voyons dès à présent comment il contribue à la sécurité de l’application.

Gestion des endpoints publics et privés

  • Endpoints publics :

Le HelloController expose un endpoint /api/public qui est accessible sans nécessiter d’authentification. Cela permet à n’importe quel utilisateur de récupérer des informations qui ne sont pas sensibles, telles que des informations générales ou des données non personnalisées. La méthode getPublic() dans le contrôleur renvoie simplement une réponse indiquant que le contenu est public. Cela peut être utilisé pour des démonstrations ou des tests de connectivité.

  • Endpoints privés :

À l’inverse, l’endpoint /api/private est sécurisé et nécessite que l’utilisateur soit authentifié. Cela est géré par Spring Security, qui intercepte les requêtes vers cet endpoint et vérifie si l’utilisateur a les droits nécessaires pour y accéder. La méthode getPrivate() renvoie une réponse indiquant que le contenu est privé, ce qui est utile pour des données sensibles ou personnalisées que seuls les utilisateurs authentifiés devraient voir.

@RestController
public class HelloController {

@GetMapping(« /api/public »)
public ResponseEntity getPublic() {
return ResponseEntity.ok(« Contenu Public »);
}
@GetMapping(« /api/private »)
public ResponseEntity getPrivate() {
return ResponseEntity.ok(« Contenu Privé »);
}
}

Contribution à la sécurité de l’application :

  • Séparation claire des niveaux d’accès : en définissant clairement quels endpoints sont publics et lesquels sont privés, HelloController aide à prévenir les accès non autorisés aux données sensibles. Cela réduit notamment le risque de fuites de données et d’autres vulnérabilités de sécurité.
  • Utilisation de Spring Security : le contrôleur tire parti des configurations de Spring Security pour appliquer des politiques d’accès. Par exemple, l’annotation @PreAuthorize pourrait être utilisée pour des vérifications d’autorisation plus fines sur les méthodes du contrôleur.
  • Audit et logging : chaque méthode peut également être enrichie avec des fonctionnalités de logging. Cela permet de tracer les accès aux endpoints et de détecter des comportements anormaux ou des tentatives d’accès non autorisées.

Création de l’application micro-frontend en Angular

Maintenant que nous avons créé une nouvelle API Spring Boot, passons à la création de l’application micro-frontend en Angular. Cela implique plusieurs étapes clés, allant de la préparation de l’environnement de développement à l’intégration des services et des composants nécessaires pour une application fonctionnelle et sécurisée. Voici un développement approfondi de chaque étape mentionnée dans la section initiale.

1. Préparation de l’environnement de développement

Avant de commencer le développement, il est essentiel de s’assurer que tous les outils nécessaires sont installés et configurés correctement. Cela inclut Node.js, NPM et Angular CLI. Ces outils fournissent le socle nécessaire pour créer et gérer une application Angular.

  • Installation de Node.js et NPM : ces outils permettent de gérer les packages nécessaires et d’exécuter l’environnement de développement local. La commande sudo apt install -y nodejs. npm permet d’installer ces outils sur des systèmes basés sur Debian/Ubuntu.
  • Vérification des versions installées : il est crucial de vérifier ensuite que les versions installées de Node.js et NPM correspondent aux exigences du projet, afin d’éviter d’éventuels problèmes de compatibilité.
  • Installation de Angular CLI : Angular CLI est un outil puissant pour initialiser, développer et maintenir des applications Angular. Il peut être installé globalement via NPM avec npm install -g @angular/cli@17.3.7.
commandes pour installer Node.js et NPM
Capture d’écran montrant les commandes pour installer Node.js et NPM, vérifier leurs versions et installer Angular CLI.

2. Installation des dépendances

Une fois l’environnement configuré, la prochaine étape consiste à installer les dépendances nécessaires pour le projet. Cela inclut les bibliothèques pour la gestion de l’état, les effets, et les interactions avec l’API backend.

  • Installation de NgRx : NgRx est une bibliothèque de gestion d’état inspirée par Redux pour Angular. L’installation des packages NgRx tels que @ngrx/store, @ngrx/effects, @ngrx/entity, et @ngrx/store-devtools permet de gérer l’état de l’application de manière prévisible et performante.
  • Installation de Bootstrap : Bootstrap est utilisé pour le stylisme rapide des composants de l’interface utilisateur, permettant une mise en page réactive et attrayante sans effort supplémentaire en CSS.

Créons maintenant nos services, composants, modules, et modèles de données. Voici les commandes à suivre :

commandes pour la création d'une application Angular et installation des dépendances NgRx
Commandes pour la création d’une application Angular et l’installation des dépendances NgRx

3. Création des services, composants et modules

La structure de l’application est définie par ses composants, services, et modules. Chaque partie joue un rôle spécifique dans l’architecture de l’application, en effet :

  • Services : les services tels que AuthService et AppService sont essentiels pour gérer la logique d’authentification et la communication avec l’API backend. Ils encapsulent la logique métier et assurent la réutilisabilité et la maintenance du code.
  • Composants : les composants tels que NavbarComponent, LoginComponent et DisplayComponent définissent les éléments de l’interface utilisateur. Chaque composant est responsable de la gestion d’une partie spécifique de l’affichage et de l’interaction utilisateur.
  • Modules : enfin, les modules Angular organisent le code en blocs fonctionnels distincts. Ils facilitent ainsi la charge paresseuse et l’isolation des différentes parties de l’application.
commandes angular CLI
Commandes Angular CLI pour générer des services d’authentification, des composants de connexion, et des modules de routage.

4. Intégration des services AuthService et AppService

L’intégration des services est cruciale pour permettre aux composants de l’application de communiquer efficacement avec l’API backend et de gérer l’état de l’authentification.

  • AuthService : ce service gère les requêtes d’authentification et la persistance du token d’utilisateur. Il joue un rôle central dans la sécurisation des routes et des données utilisateur.
  • AppService : celui-ci récupère les données nécessaires depuis le backend, servant de pont entre l’API et les composants frontend.

import { Injectable } from ‘@angular/core’;
import { HttpClient, HttpHeaders } from ‘@angular/common/http’;
import { Store, select } from ‘@ngrx/store’;
import * as AuthActions from ‘../store/auth.actions’;

import { Observable, throwError } from ‘rxjs’;
import { catchError } from ‘rxjs/operators’;
import {AuthRequest} from « ../models/auth-request »;
import {AuthResponse} from « ../models/auth-response »;
import {MyAppState} from « ../store/myApp.state »;
import * as AuthSelectors from ‘../store/auth.selectors’;

@Injectable({
providedIn: ‘root’
})
export class AuthService {

// URL de votre API
private apiUrl = ‘http://localhost:8081’;

constructor(private http: HttpClient, private store: Store) { }

// …

connect(authRequest: AuthRequest): Observable {
const headers = new HttpHeaders().set(‘Content-Type’, ‘application/json’);

return this.http.post(`${this.apiUrl}/auth/login`, authRequest, { headers }).pipe(

catchError(error => {
console.error(‘Error occurred during login: ‘, error);
return throwError(error);
})

);
}

login(authRequest: AuthRequest) {

this.store.dispatch(AuthActions.login({ authRequest }));
}

// …
isAuthenticated() {

return this.store.pipe(select(AuthSelectors.selectIsAuthenticated));
}
}

import { Injectable } from ‘@angular/core’;
import { HttpClient } from ‘@angular/common/http’;
import { Observable } from ‘rxjs’;

@Injectable({
providedIn: ‘root’
})
export class AppService {

constructor(private http: HttpClient) { }

getPrivateData(): Observable<string> {
return this.http.get<string>(‘/api/private’);
}

getPublicData(): Observable<string> {
return this.http.get<string>(‘/api/public’);
}
}

En suivant ces étapes détaillées, le développement d’une application micro-frontend en Angular devient une tâche structurée et gérable. Ce qui permet de construire une application robuste et maintenable.

5. Création des composants de l’application

À ce stade, la création des composants implique la mise en place des éléments d’interface utilisateur qui interagiront avec les utilisateurs finaux. Penchons-nous davantage sur les détails de ses différentes étapes.

NavbarComponent

Le NavbarComponent fournit une barre de navigation pour l’application. Ce composant permet donc aux utilisateurs de naviguer facilement entre les différentes sections de l’application.

  • navbar.component.html : ce fichier définit la structure HTML de la barre de navigation, incluant les liens vers les différentes parties de l’application.

<nav class=« navbar navbar-expand-lg navbar-light bg-light »>
<a class=« navbar-brand » href=« # »>My App</a>
<button class= »navbar-toggler » type= »button » data-toggle= »collapse » data-target= »#navbarNav » aria-controls= »navbarNav » aria-expanded= »false » aria-label= »Toggle navigation »>
<span class= »navbar-toggler-icon »></span>
</button>
<div class= »collapse navbar-collapse » id= »navbarNav »>
<ul class= »navbar-nav ml-auto »>
<li class= »nav-item »>
<a class= »nav-link » routerLink= »/public » href= »# » (click)= »publicScreen($event)« >Public</a>
</li>
<li class= »nav-item »>
<a class= »nav-link » routerLink= »/private » href= »# » (click)=« privateScreen($event)« >Private</a>
</li>
<li class= »nav-item » *ngIf=« !isAuthenticated« >
<a class= »nav-link » href= »# » (click)= »login($event)« >Login</a>
</li>
<li class= »nav-item » *ngIf=« isAuthenticated« >
<a class= »nav-link » href= »# » (click)= »logout($event)« >Logout</a>
</li>
</ul>
</div>
</nav>

  • navbar.component.ts : ce fichier TypeScript contient la logique nécessaire pour gérer les interactions de l’utilisateur avec la barre de navigation.

import {Component, OnInit} from ‘@angular/core’;
import {AuthService} from « ../service/auth.service »;
import {Router} from « @angular/router »;
import {NgIf} from « @angular/common »;

@Component({
selector: ‘app-navbar’,
standalone: true,
imports: [
NgIf
],
templateUrl: ‘./navbar.component.html’,
styleUrl: ‘./navbar.component.scss’
})
export class NavbarComponent {
constructor(private authService: AuthService, private router: Router) { }

publicScreen(event: Event) {
event.preventDefault();
this.router.navigate([‘/public’]);
}

privateScreen(event: Event) {
event.preventDefault();
this.router.navigate([‘/private’]);
}

login(event: Event) {
event.preventDefault();
// Add your login logic here
this.router.navigate([‘/auth/login’]);

}

logout(event: Event) {
event.preventDefault();
// Add your logout logic here
}

get isAuthenticated() {
return this.authService.isAuthenticated();
console.log(« Check if user is authenticated. »);
this.authService.isAuthenticated().subscribe(isAuthenticated => {
console.log(« isAuthenticated = « , isAuthenticated);
if (!isAuthenticated) {
console.log(« User is not authenticated. Redirect to login screen. »);
} else {
console.log(« User is authenticated. Redirect to private screen. »);
}
return isAuthenticated;

});
}

}

DisplayComponents

Les DisplayComponents sont utilisés pour afficher le contenu public et privé de l’application. Ils servent à présenter les données de manière conditionnelle en fonction de l’état d’authentification de l’utilisateur.

  • privateDisplay.component.ts et publicDisplay.component.ts : ces fichiers TypeScript définissent la logique pour afficher le contenu approprié aux utilisateurs authentifiés ou non. Les fichiers HTML associés définissent la structure du contenu à afficher pour chaque cas.

import {Component, OnInit} from ‘@angular/core’;
import {AuthService} from « ../../service/auth.service »;
import {Router} from « @angular/router »;

@Component({
selector: ‘app-display’,
templateUrl: ‘./display.component.html’,
styleUrl: ‘./display.component.scss’
})
export class DisplayComponent implements OnInit {

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

ngOnInit(): void {
console.log(« Check if user is authenticated. »);
this.authService.isAuthenticated().subscribe(isAuthenticated => {
console.log(« isAuthenticated = « , isAuthenticated);
if (!isAuthenticated) {
console.log(« User is not authenticated. Redirect to login screen. »);
this.router.navigate([‘/auth/login’]);
} else {
console.log(« User is authenticated. Redirect to private screen. »);
this.router.navigate([‘/private’]);
}
});
}

}

import { Component } from ‘@angular/core’;

@Component({
selector: ‘app-display’,
templateUrl: ‘./display.component.html’,
styleUrl: ‘./display.component.scss’
})
export class DisplayComponent {

}

AuthentificationComponent et LoginComponent

Le LoginComponent est responsable de la gestion de l’authentification des utilisateurs. Ce composant fournit une interface permettant aux utilisateurs de se connecter à l’application.

  • authlogin.component.html : ce fichier HTML crée le formulaire de connexion, incluant les champs pour le nom d’utilisateur et le mot de passe.
  • login.component.ts : le fichier TypeScript associé gère la soumission du formulaire de connexion et communique avec le service d’authentification pour vérifier les identifiants de l’utilisateur.

<p>login works!

<form [formGroup]= »loginForm » (ngSubmit)= »onSubmit() »>
<div>
<label for= »username »>Username</label>
<input id= »username » type= »text » formControlName= »username »>
</div>
<div>
<label for= »password »>Password</label>
<input id= »password » type= »password » formControlName= »password »>
</div>
<button type= »submit »>Login</button>
</form>

import {Component, OnInit} from ‘@angular/core’;
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from « @angular/forms »;
import { Store } from ‘@ngrx/store’;
import { Router } from « @angular/router »;
import { AuthRequest } from « ../../models/auth-request »;
import * as AuthActions from ‘../../store/auth.actions’;
import { MyAppState } from ‘../../store/myApp.state’;

@Component({
selector: ‘app-login’,
templateUrl: ‘./login.component.html’,
styleUrl: ‘./login.component.scss’
})
export class LoginComponent implements OnInit {
loginForm: FormGroup = this.formBuilder.group({
username: [ », Validators.required],
password: [ », Validators.required] });

constructor(
private formBuilder: FormBuilder,
private store: Store,
private router: Router
) { }

ngOnInit(): void {
this.loginForm = this.formBuilder.group({
username: [ », Validators.required],
password: [ », Validators.required] });
}

onSubmit(): void {
if (this.loginForm.valid) {
const authRequest = new AuthRequest();
authRequest.username = this.loginForm.value.username;
authRequest.password = this.loginForm.value.password;

// Dispatch the login action
this.store.dispatch(AuthActions.login({ authRequest }));
} else {
console.error(‘Form is not valid’);
// Handle invalid form, e.g. show a message to the user
}
}
}

6. Mise en place de NGRX pour la gestion de l’état

NGRX offre une solution robuste pour la gestion de l’état de l’application de manière prévisible. L’utilisation de NGRX implique la création de plusieurs fichiers pour configurer le store, les actions, les reducers et les selectors.

  • auth.actions.ts, auth.effects.ts, auth.reducer.ts, auth.selectors.ts, myApp.state.ts : Ces fichiers constituent le cœur de la configuration de NGRX, permettant de gérer l’état d’authentification dans l’application.

import { createAction, props } from ‘@ngrx/store’;
import { AuthResponse } from ‘../models/auth-response’;
import {AuthRequest} from « ../models/auth-request »;

export const login = createAction(
‘[Auth] Login’,
props<{ authRequest: AuthRequest }>()
);

export const loginSuccess = createAction(
‘[Auth] Login Success’,
props<{ authResponse: AuthResponse }>()
);

export const loginFailure = createAction(
‘[Auth] Login Failure’,
props<{ error: any }>()
);

// src/app/store/auth.effects.ts
import { Injectable } from ‘@angular/core’;
import { Actions, createEffect, ofType } from ‘@ngrx/effects’;
import { catchError, map, mergeMap } from ‘rxjs/operators’;
import { of } from ‘rxjs’;

import { AuthService } from ‘../service/auth.service’;
import * as AuthActions from ‘./auth.actions’;

@Injectable()
export class AuthEffects {

login$ = createEffect(() =>
this.actions$.pipe(

ofType(AuthActions.login),
mergeMap(action =>
this.authService.connect(action.authRequest).pipe(
map(authResponse => AuthActions.loginSuccess({ authResponse })),
catchError(error => of(AuthActions.loginFailure({ error })))

)
)
)
);

constructor(
private actions$: Actions,
private authService: AuthService
) {}
}

import { createReducer, on } from ‘@ngrx/store’;
import { login, loginSuccess, loginFailure } from ‘./auth.actions’;
import { initialState, MyAppState } from ‘./myApp.state’;

export const authReducer = createReducer(
initialState,
on(login, state => ({ …state, isAuthenticated: false })),
on(loginSuccess, (state, { authResponse }) => ({ …state, isAuthenticated: true, token: authResponse.token })),
on(loginFailure, (state, { error }) => ({ …state, isAuthenticated: false, error }))
);

// src/app/store/auth.selectors.ts
import { createFeatureSelector, createSelector } from ‘@ngrx/store’;
import {MyAppState} from « ./myApp.state »;
export const selectAuthState = createFeatureSelector(‘auth‘);
export const selectIsAuthenticated = createSelector(
selectAuthState,
(state: MyAppState) => state.isAuthenticated
);

// src/app/store/state.ts
export interface MyAppState {
isAuthenticated: boolean;
token: string | null;
error: any;
}

export const initialState: MyAppState = {
isAuthenticated: false,
token: null,
error: null

};

7. Les modules

Les modules Angular sont des conteneurs dédiés à un ensemble cohérent de fonctionnalités d’une application. Ils permettent de structurer le code et de gérer les dépendances de manière efficace.

Module d’authentification

Le module AuthModule est responsable de toutes les fonctionnalités liées à l’authentification.

  • CommonModule : fournit des directives et des services communs nécessaires pour les composants de ce module.
  • AuthRoutingModule : contient les routes spécifiques à l’authentification.
  • LoginComponent : composant responsable de l’affichage du formulaire de connexion.
  • ReactiveFormsModule : utilisé pour créer et gérer des formulaires réactifs.

import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;

import { AuthRoutingModule } from ‘./auth-routing.module’;
import {LoginComponent} from « ./login/login.component »;
import {ReactiveFormsModule} from « @angular/forms »;

@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
AuthRoutingModule,
ReactiveFormsModule
] })
export class AuthModule { }

Module de l’interface privé

Le module PrivateModule gère les fonctionnalités privées accessibles uniquement aux utilisateurs authentifiés.

  • DisplayComponent : composant utilisé pour afficher les données privées.
  • KeycloakService : service utilisé pour l’authentification et l’autorisation via Keycloak.
  • HttpClientModule : utilisé pour effectuer des requêtes HTTP.

import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;

import { PrivateRoutingModule } from ‘./private-routing.module’;
import {DisplayComponent} from « ./display/display.component »;
import {KeycloakService} from « keycloak-angular »;
import {HttpClientModule} from « @angular/common/http »;

@NgModule({
declarations: [
DisplayComponent
],
imports: [
CommonModule,
PrivateRoutingModule,
HttpClientModule
],
providers: [KeycloakService] })
export class PrivateModule { }

8. La configuration des routes via les modules

La configuration des routes est essentielle pour naviguer entre les différentes vues de l’application Angular. Elle permet de définir quelles vues doivent être affichées pour chaque URL.

Module AuthRouting

Le module de routage AuthRoutingModule gère les routes pour les composants d’authentification.

  • LoginComponent : définition de la route pour le composant de connexion. Cette route sera accessible via /auth/login.

import { NgModule } from ‘@angular/core’;
import { RouterModule, Routes } from ‘@angular/router’;
import {LoginComponent} from « ./login/login.component »;

const routes: Routes = [
{ path: ‘login‘, component: LoginComponent }
];

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

Module PrivateRouting

Le module de routage PrivateRoutingModule gère les routes pour les composants privés.

  • DisplayComponent : définition de la route pour le composant privé. Cette route sera accessible via /private.

import { NgModule } from ‘@angular/core’;
import { RouterModule, Routes } from ‘@angular/router’;
import {DisplayComponent} from « ./display/display.component »;

const routes: Routes = [
{ path:  », component: DisplayComponent }
];

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

Module PublicRouting

Le module de routage PublicRoutingModule gère quant à lui les routes pour les composants publics.

  • DisplayComponent : définition de la route pour le composant public. Cette route sera accessible via /public.

import { NgModule } from ‘@angular/core’;
import { RouterModule, Routes } from ‘@angular/router’;
import {DisplayComponent} from « ./display/display.component »;

const routes: Routes = [
{ path:  », component: DisplayComponent }
];

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

Module AppRouting

Pour finir, le module de routage principal AppRoutingModule gère les routes globales de l’application.

  • loadChildren : permet de charger les modules de manière asynchrone.
  • redirectTo : redirige les utilisateurs vers la route /public par défaut.

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

const routes: Routes = [
{ path: ‘auth’, loadChildren: () => import(‘./auth/auth.module’).then(m => m.AuthModule) },
{ path: ‘public’, loadChildren: () => import(‘./public/public.module’).then(m => m.PublicModule) },
{ path: ‘private’, loadChildren: () => import(‘./private/private.module’).then(m => m.PrivateModule) },
{ path:  », redirectTo: ‘/public’, pathMatch: ‘full’ }
];

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

9. Mise à jour de l’app.module

Le fichier app.module.ts agit comme le cœur de l’application Angular, orchestrant la coordination entre les différents modules, services, et composants. Voici les principales configurations réalisées dans ce fichier :

  • Importation des modules nécessaires : tous les modules nécessaires pour l’application. Les modules Angular internes comme BrowserModule et HttpClientModule, ainsi que les modules personnalisés comme NavbarComponent et les modules de routage sont importés ici.
  • Déclarations des composants : tous les composants utilisés dans l’application sont déclarés dans ce module. Cela inclut les composants pour la navigation, l’authentification, et l’affichage des données publiques et privées.
  • Fournisseurs de services : les services tels que AuthService et AppService qui gèrent la logique d’authentification et la communication avec l’API backend, sont déclarés comme fournisseurs. Cela garantit qu’ils peuvent être injectés et utilisés dans n’importe quel composant de l’application.
  • Intégration de NgRx : le StoreModule et EffectsModule de NgRx sont configurés pour gérer l’état global de l’application. Cela inclut la configuration des reducers et des effets, qui aident à gérer l’état de l’authentification et à traiter les effets secondaires comme les interactions avec l’API externe.

import {APP_INITIALIZER, NgModule} from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;

import { AppRoutingModule } from ‘./app-routing.module’;
import { AppComponent } from ‘./app.component’;
import {NavbarComponent} from « ./navbar/navbar.component »;
import {KeycloakAngularModule} from « keycloak-angular »;
import {HttpClientModule} from « @angular/common/http »;

import {ActionReducer, MetaReducer, StoreModule} from ‘@ngrx/store’;
import { EffectsModule } from ‘@ngrx/effects’;
import { authReducer } from ‘./store/auth.reducer’;
import { AuthEffects } from ‘./store/auth.effects’;

import { StoreDevtoolsModule } from ‘@ngrx/store-devtools’;
import { environment } from ‘../environment’;
import {localStorageSync} from « ngrx-store-localstorage »;
import {CommonModule} from « @angular/common »; // import the environment file

export function localStorageSyncReducer(reducer: ActionReducer<any>): ActionReducer<any> {
return localStorageSync({ keys: [‘auth’], rehydrate: true })(reducer);
}
const metaReducers: Array<MetaReducer<any, any>> = [localStorageSyncReducer];

@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
CommonModule,
NavbarComponent,
KeycloakAngularModule,
HttpClientModule, //

// …
StoreModule.forRoot({ auth: authReducer }, { metaReducers }),
EffectsModule.forRoot([AuthEffects]),
!environment.production ? StoreDevtoolsModule.instrument() : [] // Add this line

],
providers: [],
bootstrap: [AppComponent] })
export class AppModule { }

Test de l’application

Maintenant que notre application est configurée, il est temps de la tester. Dans cette nouvelle section, nous allons commencer par effectuer une installation propre des dépendances, puis démarrer l’application et vérifier les différents écrans via l’interface utilisateur.

1. Installation des dépendances

Tout d’abord, nous allons utiliser Maven pour effectuer une installation propre des dépendances. Assurez-vous d’être dans le répertoire racine de votre projet Spring Boot.

Commande Maven pour nettoyer le projet et installer les dépendances
Commande Maven pour nettoyer le projet et installer les dépendances

Cette commande va nettoyer le projet, télécharger, installer toutes les dépendances nécessaires, et compiler le code.

2. Démarrage de l’application Angular

Ensuite, naviguez vers le répertoire de votre application Angular et démarrez le serveur de développement.

Commande pour démarrer le serveur de développement avec npm start
Commande pour démarrer le serveur de développement avec npm start

Cette commande va lancer le serveur de développement Angular et votre application sera disponible à l’adresse suivante : http://localhost:4200/

3. Test des écrans public et privé

Maintenant que l’application est en cours d’exécution, nous allons tester les différents écrans.

1. Écran Public :

  • Accédez à l’URL http://localhost:4200/public.
  • Vous devriez voir l’écran public s’afficher directement sans demander d’authentification.
interface utilisateur application Angular
Capture d’écran affichant l’interface utilisateur d’une application Angular en mode développement, avec un message confirmant le fonctionnement de l’affichage public

2. Écran privé :

  • Accédez à l’URL http://localhost:4200/private.
  • Si l’utilisateur est déjà authentifié, l’écran privé s’affiche directement.
  • Sinon, vous serez redirigé vers l’écran de connexion.
interface de connexion angular
Capture d’écran affichant l’interface de connexion

4. Authentification et accès à l’écran privé

Sur l’écran de connexion, suivez les étapes suivantes :

  1. Saisissez votre nom d’utilisateur et votre mot de passe.
  2. Cliquez sur le bouton de connexion.

Voici ce qu’il doit se passer ensuite :

  • Le frontend envoie une requête au backend pour vérifier les informations d’identification.
  • Le backend communique avec Keycloak pour générer un token basé sur les informations d’identification fournies et les autres paramètres Keycloak.
  • Si les données de l’utilisateurs sont éronnées, il retourne un statut = KO qui signifie en back qu’il y a eu un 401 Unauthorized au frontend.
  • Dans notre cas, nous avons envoyer :
    – Username : my-user
    – password = my-passwor. (éronné)
section 'Request Payload' outil de développement
Capture d’écran montrant la section ‘Request Payload’ d’un outil de développement web, où des données d’authentification sont envoyées pour obtenir un token d’accès

Sur la console de chrome, nous voyons le retour de réponse :

erreur HTTP 401 mise en place d'une application micro frontend pour consommer l’API
Capture d’écran montrant une erreur HTTP 401 non autorisée lors d’une tentative de connexion à l’API locale, indiquant un problème d’authentification avec les détails de la requête et de la réponse.

Du côté du network, nous observons le 401 Unauthorized dans les headers :

Capture d’écran de l’inspecteur web montrant les détails d’une requête HTTP avec un code d’état 401 Unauthorized

La réponse retournée par le back est alors un Statut KO, comme nous pouvons l’observer dans la capture d’écran ci-dessous :

réponse d'échec d'authorisation mise en place d'une application micro frontend pour consommer l’API
Capture d’écran de l’inspecteur web montrant les détails de la réponse en cas d’echec d’authorisation

À noter que, si le token est valide, il est retourné au frontend avec un statut « OK ».
Dans la capture d’écran ci-dessous, nous observons que le Statuts Code correspond à 200 OK, avec un statut ‘OK’. De plus, la réponse a renvoyé le token et le username au frontend.

Capture d’écran affichant l’interface utilisateur d’une application Angular en mode développement, avec un message confirmant le fonctionnement de l’affichage de la page privé suite à une authentification
Inspecteur web de l'application micro frontend consommant l'API
Capture d’écran de l’inspecteur web montrant les détails de la réponse en cas d’authorisation avec succès

Pour finir, sur la capture d’écran ci-dessous, nous pouvons voir que :

  • Le frontend stocke le token ainsi qu’une variable d’authentification isAuthentified dans le store NGRX. Il  utilise également ce dernier pour accéder à l’endpoint /private du backend.
  • Le token est passé dans l’en-tête de la requête HTTP au backend.
  • Le backend vérifie le token et retourne le contenu de la page privée.
  • Le frontend redirige ensuite l’utilisateur vers le contenu de la page privée et modifie le bouton login dans le navbar par le bouton logout.

Dans le code source vous pourrez trouver plus de détails, ainsi que l’implémentation du mécanisme de logout et des améliorations de sécurité pour l’authentification.

Conclusion

Pour conclure, récapitulons rapidement les points clés vus dans ce quatrième épisode. Nous avons détaillé :

  • L’importance de la sécurité dans une architecture micro-frontend et comment OAuth2 et OpenID Connect avec Keycloak peuvent vous aider à sécuriser vos API.
  • La préparation de l’environnement de développement en installant les versions nécessaires de Node.js, NPM, Angular CLI, et en utilisant IntelliJ IDE.
  • La création d’une API Spring Boot sécurisée avec les endpoints /auth/login et /auth/logout et la configuration de Spring Security.
  • Le développement d’une application micro-frontend en Angular avec les services AuthService et AppService, les composants Navbar, Private, Public, et Login, ainsi que l’intégration de NGRX pour la gestion de l’état.
  • La création de modules et la configuration des routes pour organiser et naviguer efficacement dans l’application.

En suivant les différentes étapes détaillées ici, vous devriez avoir une architecture modulaire et sécurisée prête à être déployée.

Ne manquez pas le prochain épisode

Ressources complémentaires

Voici également quelques ressources supplémentaires pour approfondir vos connaissances :

Démo

Pour voir une démo complète de cette application, vous pouvez cloner les repo Github suivants :

Pour cloner le repository Git de l’application Frontend :

cloner repository git application micro-frontend
À propos de l’auteur
Photo de profil Linkedin de Adnene Hamdouni

ADNENE HAMDOUNI,

Icon linkedIn
Icone Github

Partager