Comme les technologies Web continuent à évoluer, de même font les méthodes et protocoles conçus pour les sécuriser. Les protocoles OAuth 2.0 et OpenID Connect ont évolué significativement en réponse aux nouvelles menaces de sécurité émergentes et à la complexité croissante des applications Web. Les méthodes traditionnelles d’authentification, qui étaient autrefois efficaces, sont maintenant devenues obsolètes pour les Applications à Page Unique (APU) modernes, qui font face à de nouveaux défis de sécurité. Dans ce contexte, le patron architectural Backend-For-Frontend (BFF) a émergé en tant que solution recommandée pour organiser les interactions entre les APU et leurs systèmes backend, offrant une approche plus sécurisée et plus facile à gérer pour l’authentification et la gestion des sessions. Cet article examine en profondeur le patron BFF, montrant son application pratique par une solution minimale mise en œuvre avec .NET et React. Au final, vous aurez une compréhension claire de comment exploiter le patron BFF pour améliorer la sécurité et les fonctionnalités de vos applications Web.
Contexte Historique
L’histoire d’OAuth 2.0 et d’OpenID Connect reflète l’évolution continue des technologies Internet. Voyons plus près ces protocoles et leur impact sur les applications Web modernes.
Inventé en 2012, le protocole OAuth 2.0 est devenu une norme largement adoptée pour l’autorisation. Il permet aux applications tierces d’obtenir un accès limité aux ressources de l’utilisateur sans exposer ses identifiants au client. OAuth 2.0 supporte plusieurs flux, chacun conçu pour s’adapter avec flexibilité à divers cas d’utilisation.
Sur la base du protocole OAuth 2.0, le protocole OpenID Connect (OIDC) a vu le jour en 2014, en ajoutant des fonctionnalités d’authentification essentielles. Il fournit aux applications clientes une méthode standard pour vérifier l’identité de l’utilisateur et obtenir des informations de base sur lui par un point d’accès standardisé ou en acquérant un jeton d’identité au format JWT (JSON Web Token).
Évolution du Modèle de Menaces
Avec les capacités croissantes et la popularité croissante des SPAs, le modèle de menaces pour les SPAs a également évolué. Les vulnérabilités telles que l’Injection de Script Cross-Site (XSS) et la Fraude Cross-Site Request Forgery (CSRF) sont devenues plus répandues. Comme les SPAs interagissent souvent avec le serveur via des API, le stockage sécurisé et l’utilisation de jetons d’accès et de rafraîchissement sont devenus cruciaux pour la sécurité.
Répondant aux exigences des temps, les protocoles OAuth et OpenID Connect continuent d’évoluer pour s’adapter aux nouveaux défis qui apparaissent avec les nouvelles technologies et le nombre croissant de menaces. Parallèlement, l’évolution constante des menaces et l’amélioration des pratiques de sécurité signifient que les approches périmées ne satisfont plus aux exigences de sécurité modernes. Ainsi, le protocole OpenID Connect offre actuellement une large gamme de fonctionnalités, mais beaucoup d’entre elles sont déjà considérées comme obsolètes et souvent non sécurisées. Cette diversité pose des difficultés aux développeurs de SPAs pour choisir la méthode la plus appropriée et la plus sécurisée pour interagir avec le serveur OAuth 2.0 et OpenID Connect.
En particulier, le flux implicite est désormais considéré obsolète, et pour tout type de client, qu’il s’agisse d’un SPA, d’une application mobile ou d’une application de bureau, il est fortement recommandé d’utiliser le flux d’autorisation code accompagné de la clé de preuve pour l’échange de code (PKCE).
Sécurité des SPAs modernes
Pourquoi les SPAs modernes sont-elles toujours considérées vulnérables, même en utilisant le flux d’autorisation code avec PKCE ? Plusieurs réponses sont possibles à cette question.
Vulnérabilités de code JavaScript
JavaScript est un langage de programmation puissant qui joue un rôle clé dans les applications uniques de page (SPA) modernes. Cependant, ses vastes capacités et sa prépondérance posent une menace potentielle. Les SPAs modernes construites sur les bibliothèques et les frameworks telles que React, Vue ou Angular utilisent un nombre considérable de bibliothèques et de dépendances. Vous pouvez les voir dans le dossier node_modules
, et le nombre de telles dépendances peut atteindre les centaines ou même les milliers. Chaque de ces bibliothèques peut contenir des vulnérabilités d’importance variable, et les développeurs de SPA n’ont pas la possibilité de vérifier à fond le code de toutes les dépendances utilisées. souvent, les développeurs n’evenementiels pas même la liste complète des dépendances, car elles sont dépendantes les unes des autres de manière transitive. Même en développant leur propre code selon les plus hautes normes de qualité et de sécurité, on ne peut pas être complètement sûr de l’absence de vulnérabilités dans l’application finale.
Code JavaScript malveillant, qui peut être injecté dans une application de diverses manières, par des attaques telles que l’Injection de Script Cross-Site (XSS) ou par la compromission de bibliothèques tierces, obtient les mêmes privilèges et niveau d’accès aux données que le code application légitime. Cela permet au code malveillant de voler des données de la page actuelle, interagir avec l’interface de l’application, envoyer des demandes vers l’arrière-plan, voler des données du stockage local (localStorage, IndexedDB) et mener même des sessions d’authentification lui-même, en obtenant ses propres tokens d’accès en utilisant le même Code d’Autorisation et le même flux PKCE.
Vulnérabilité Spectre
La vulnérabilité Spectre exploite les caractéristiques de l’architecture des processeurs modernes pour accéder à des données qui devraient être isolées. De telles vulnérabilités sont particulièrement dangereuses pour les SPA.
Premièrement, les SPA utilisent intensément le JavaScript pour gérer l’état de l’application et interagir avec le serveur. Cela augmente la surface d’attaque pour le code JavaScript malveillant pouvant exploiter les vulnérabilités Spectre. Deuxièmement, contrairement aux applications traditionnelles à plusieurs pages (MPAs), les SPA ne sont rarement rechargées, ce qui signifie que la page et son code chargé restent actifs pendant une longue période. Cela donne aux attaquants beaucoup plus de temps pour effectuer des attaques à l’aide de code JavaScript malveillant.
Les vulnérabilités Spectre permettent aux attaquants de voler des tokens d’accès stockés dans la mémoire d’une application JavaScript, permettant l’accès à des ressources protégées en se faisant passer pour l’application légitime. L’exécution spéculative peut également être utilisée pour voler des données de session utilisateur, permettant aux attaquants de poursuivre leurs attaques même après que l’SPA ait été fermé.
Le fait que d’autres vulnérabilités similaires à Spectre puissent être découvertes dans l’avenir ne peut être exclué.
Quoi faire ?
Permettez-moi de résumer une conclusion intermédiaire importante. Les SPAs modernes, dépendants d’un grand nombre de bibliothèques JavaScript tiers et exécutés dans l’environnement du navigateur sur les appareils utilisateurs, fonctionnent dans un environnement logiciel et matériel que les développeurs ne peuvent pas totalement contrôler. Par conséquent, nous devrions considérer de tels applications comme intrinsèquement vulnérables.
Afin de faire face aux menaces énumérées, de nombreux experts se tournent vers l’évitement complet de l’enregistrement des tokens dans le navigateur et vers la conception de l’application de sorte que les tokens d’accès et de rafraîchissement soient obtenus et traités uniquement par le côté serveur de l’application, et qu’ils ne soient jamais transmis vers le côté navigateur. Dans le contexte d’un SPA avec un backend, cela peut être réalisé en utilisant le patron d’architecture Backend-For-Frontend (BFF).
Le schéma d’interaction entre le serveur d’autorisation (OP), le client (RP) appliquant le patron BFF et une API tiers (Serveur de ressources) ressemble à ceci :
L’utilisation du patron BFF pour protéger les SPAs offre plusieurs avantages. Les tokens d’accès et de rafraîchissement sont stockés au serveur et ne sont jamais transmis vers le navigateur, évitant ainsi leur vol à cause de vulnérabilités. La gestion de la session et des tokens est gérée au serveur, permettant un meilleur contrôle de sécurité et une vérification d’authentification plus fiable. L’application cliente interagit avec le serveur par le biais du BFF, ce qui simplifie la logique de l’application et réduit le risque d’exécution de code malveillant.
Implémenter le patron Backend-For-Frontend sur la plateforme .NET.
Avant de passer à la mise en œuvre pratique du BFF sur la plateforme .NET, considérons ses composants nécessaires et planifions nos actions. Supposons que nous avons déjà un serveur OpenID Connect configuré et qu’il nous faut développer un SPA qui fonctionne avec un backend, implémenter l’authentification en utilisant OpenID Connect, et organiser l’interaction entre les parties serveur et client en utilisant le patron BFF.
Selon le document OAuth 2.0 for Browser-Based Applications, le patron architectural BFF prévoit que le backend agisse en tant que client OpenID Connect, utilise le flux d’authentification Code d’Autorisation avec PKCE, obtient et stocke les jetons d’accès et de rafraîchissement sur son propre côté, et ne les passe jamais à la partie SPA dans le navigateur. Le patron BFF prévoit également la présence d’une API sur le côté backend composée de quatre points de terminaison principaux :
- Check Session : permet de vérifier une session d’authentification active de l’utilisateur. Elle est généralement appelée par l’SPA en utilisant une API asynchrone (fetch) et, si réussie, retourne des informations sur l’utilisateur actif. Ainsi, l’SPA, chargé à partir d’une source tiers (par exemple, CDN), peut vérifier l’état d’authentification et soit continuer son travail avec l’utilisateur, soit procéder à l’authentification en utilisant le serveur OpenID Connect.
- Connexion : démarre le processus d’authentification sur le serveur OpenID Connect. Habituellement, si l’SPA échoue à obtenir les données d’utilisateur authentifié à l’étape 1 par l’intermédiaire de Check Session, il redirige le navigateur vers cette URL, qui forme ensuite une requête complète vers le serveur OpenID Connect et redirige le navigateur là-vers.
- Se connecter : reçoit l’Code d’autorisation envoyé par le serveur après l’étape 2 dans le cas de succès de l’authentification. Effectue une requête directe vers le serveur OpenID Connect pour échanger l’Code d’autorisation + le Code verificateur PKCE pour les jetons d’accès et de rafraîchissement. Initie une session authentifiée côté client en émettant un cookie d’authentification à l’utilisateur.
- Déconnexion : sert à terminer la session d’authentification. Habituellement, l’SPA redirige le navigateur vers cette URL, qui à son tour forme une requête vers l’extrémité End Session sur le serveur OpenID Connect pour terminer la session, ainsi que pour supprimer la session côté client et le cookie d’authentification.
Maintenant, examinons les outils que la plateforme .NET fournit de base et regardons ce que nous pouvons utiliser pour implémenter le patron BFF. La plateforme .NET offre le package NuGet Microsoft.AspNetCore.Authentication.OpenIdConnect
, qui est une implémentation prête à l’emploi d’un client OpenID Connect supporté par Microsoft. Ce package supporte à la fois le flux d’autorisation Code et PKCE, et il ajoute une extrémité avec le chemin relatif /signin-oidc, qui implémente déjà les fonctionnalités nécessaires de l’extrémité Sign In (voir point 3 ci-dessus). Ainsi, nous devons implémenter seulement les trois extrémités restantes.
Pour un exemple pratique d’intégration, nous allons utiliser un serveur OpenID Connect de test basé sur la bibliothèque Abblix OIDC Server. Cependant, tout ce qui est mentionné ci-dessous s’applique également à n’importe quel autre serveur, y compris les serveurs publics de Facebook, Google, Apple et d’autres suivant la spécification du protocole OpenID Connect.
Pour implémenter le SPA sur le côté frontend, nous utiliserons la bibliothèque React, et sur le côté backend, nous utiliserons .NET WebAPI. C’est l’une des configurations de technologie les plus communes à l’époque de l’écriture de cet article.
La schéma global des composants et de leur interaction ressemble à ceci :
Pour travailler avec les exemples de cet article, vous devrez également installer le .NET SDK et Node.js. Tous les exemples de cet article ont été développés et testés à l’aide de .NET 8, Node.js 22 et React 18, qui étaient les versions courantes à l’époque de l’écriture.
Créer un SPA client sur React avec un backend sur .NET
Pour créer rapidement une application cliente, il est pratique d’utiliser une modèle prédéfinie. Jusqu’à la version .NET 7, l’SDK offrait une modèle intégré pour une application .NET WebAPI et une application SPA React. Malheureusement, ce modèle a été retiré dans la version .NET 8. C’est pourquoi l’équipe Abblix a créé son propre modèle, qui inclut un backend .NET WebApi, un frontend SPA basé sur la bibliothèque React et TypeScript, construit avec Vite. Ce modèle est disponible au public en tant que package Abblix.Templates
, et vous pouvez l’installer en exécutant la commande suivante :
dotnet new install Abblix.Templates
Maintenant, nous pouvons utiliser le modèle nommé abblix-react
. Utilisons-le pour créer une nouvelle application appelée BffSample
:
dotnet new abblix-react -n BffSample
Cette commande crée une application composée d’un backend .NET WebApi et d’un client SPA React. Les fichiers liés à l’SPA se trouvent dans le dossier BffSample\ClientApp
.
Après la création du projet, le système vous invite à exécuter une commande pour installer les dépendances :
cmd /c "cd ClientApp && npm install"
Cette action est nécessaire pour installer toutes les dépendances requises pour la partie cliente de l’application. Pour un lancement réussi du projet, il est recommandé d’accepter et d’exécuter cette commande en entrant Y
(oui).
Changons immédiatement le numéro de port sur lequel l’application BffSample
fonctionne localement en 5003. Cette action n’est pas obligatoire, mais elle simplifiera la configuration du serveur OpenID Connect. Pour ce faire, ouvrez le fichier BffSample\Properties\launchSettings.json
, trouvez le profil nommé https
et changez la valeur de la propriété applicationUrl
en https://localhost:5003
.
Puis-à-dire, ajouter le paquet NuGet implémentant le client OpenID Connect en naviguant vers le dossier BffSample
et en exécutant la commande suivante :
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
Configurer deux schémas d’authentification nommés Cookies
et OpenIdConnect
dans l’application, en lisant leurs réglages à partir de la configuration de l’application. Pour ce faire, modifier le fichier BffSample\Program.cs
comme suit :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ******************* DÉBUT *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************** FIN ********************
var app = builder.Build();
Ajouter les réglages nécessaires pour se connecter au serveur OpenID Connect dans le fichier BffSample\appsettings.json
:
{
// ******************* DÉBUT *******************
"Authentication": {
"DefaultScheme": "Cookies",
"DefaultChallengeScheme": "OpenIdConnect"
},
"OpenIdConnect": {
"SignInScheme": "Cookies",
"SignOutScheme": "Cookies",
"SaveTokens": true,
"Scope": ["openid", "profile", "email"],
"MapInboundClaims": false,
"ResponseType": "code",
"ResponseMode": "query",
"UsePkce": true,
"GetClaimsFromUserInfoEndpoint": true
},
// ******************** FIN ********************
"Logging": {
"LogLevel": {
"Default": "Information",
Et dans le fichier BffSample\appsettings.Development.json
:
{
// ******************* DÉBUT *******************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
"ClientSecret": "secret"
},
// ******************** FIN ********************
"Logging": {
"LogLevel": {
"Default": "Information",
Revenons brièvement sur chaque réglage et son objectif :
Authentification
section : La propriétéDefaultScheme
définit l’authentification par défaut en utilisant le schémaCookies
, etDefaultChallengeScheme
délègue l’exécution de l’authentification au schémaOpenIdConnect
lorsque l’utilisateur ne peut pas être authentifié par le schéma par défaut. Ainsi, lorsque l’utilisateur est inconnu de l’application, le serveur OpenID Connect sera appelé pour l’authentification, et après cela, l’utilisateur authentifié recevra un cookie d’authentification, et tous les appels ultérieurs au serveur seront authentifiés avec celui-ci, sans contacter le serveur OpenID Connect.OpenIdConnect
section :SignInScheme
etSignOutScheme
définissent le schémaCookies
, qui sera utilisé pour sauvegarder les informations de l’utilisateur après l’identification.- La propriété
Authority
contient l’URL de base du serveur OpenID Connect.ClientId
etClientSecret
spécifient l’identifiant et la clé secrète de l’application cliente, inscrits sur le serveur OpenID Connect. SaveTokens
indique la nécessité de sauvegarder les jetons reçus en résultant de l’authentification auprès du serveur OpenID Connect.Scope
contient une liste des emplacements que l’applicationBffClient
demande l’accès à. Dans ce cas, les emplacements standardsopenid
(identifiant de l’utilisateur),profile
(profil de l’utilisateur) etemail
(courriel) sont demandés.MapInboundClaims
est responsable de la transformation des revendications entrantes du serveur OpenID Connect en revendications utilisées dans l’application. Une valeur defalse
signifie que les revendications seront sauvegardées dans la session de l’utilisateur authentifié sous la forme dans laquelle elles sont reçues du serveur OpenID Connect.ResponseType
avec la valeurcode
indique que le client utilisera le Flux d’autorisation Code.ResponseMode
spécifie la transmission du Code d’autorisation dans la chaîne de requête, qui est la méthode par défaut pour le Flux d’autorisation Code.- La propriété
UsePkce
indique la nécessité d’utiliser PKCE lors de l’authentification pour empêcher l’interception du Code d’autorisation. - La propriété
GetClaimsFromUserInfoEndpoint
indique que les données du profil utilisateur devraient être obtenues à partir de l’extrémité UserInfo.
Comme notre application n’entend pas deinteraction avec l’utilisateur sans autentification, nous verrons à assurer que le composant React SPA soit chargé uniquement après une authentification réussie. Bien sûr, si le SPA est chargé à partir d’une source externe telle qu’un Hôte Web Statique, par exemple des serveurs de Content Delivery Network (CDN) ou un serveur de développement local lancé avec la commande npm start
(par exemple, lors du déploiement de notre exemple en mode de débug), il sera impossible de vérifier l’état d’authentification avant le chargement du SPA. Cependant, lorsque notre backend .NET est responsable du chargement du SPA, cela est possible.
Pour ce faire, ajoutez le middleware responsable de l’authentification et de l’autorisation dans le fichier BffSample\Program.cs
:
app.UseRouting();
// ******************* DÉBUT *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************** FIN ********************
A la fin du fichier BffSample\Program.cs
, où la transition vers le chargement du SPA est directement effectuée, ajoutez l’exigence d’autorisation, .RequireAuthorization()
:
app.MapFallbackToFile("index.html").RequireAuthorization();
Configuration du serveur OpenID Connect.
Comme mentionné plus tôt, pour l’exemple pratique d’intégration, nous utiliserons un serveur de test OpenID Connect basé sur la bibliothèque Abblix OIDC Server. La template de base pour une application basée sur ASP.NET Core MVC avec la bibliothèque Abblix OIDC Server
est également disponible dans le package Abblix.Templates
que nous avons installé plus tôt. Utilisons cette template pour créer une nouvelle application nommée OpenIDProviderApp
:
dotnet new abblix-oidc-server -n OpenIDProviderApp
Pour configurer le serveur, nous devons enregistrer l’application BffClient
en tant que client sur le serveur OpenID Connect et ajouter un utilisateur de test. Pour ce faire, ajoutez les blocs suivants au fichier OpenIDProviderApp\Program.cs
:
var userInfoStorage = new TestUserStorage(
// ******************* DÉBUT *******************
new UserInfo(
Subject: "1234567890",
Name: "John Doe",
Email: "[email protected]",
Password: "Jd!2024$3cur3")
// ******************** FIN ********************
);
builder.Services.AddSingleton(userInfoStorage);
// ...
// Enregistrez et configurez le serveur Abblix OIDC
builder.Services.AddOidcServices(options =>
{
// Configurez les options du serveur OIDC ici :
// ******************* DÉBUT *******************
options.Clients = new[] {
new ClientInfo("bff_sample") {
ClientSecrets = new[] {
new ClientSecret {
Sha512Hash = SHA512.HashData(Encoding.ASCII.GetBytes("secret")),
}
},
TokenEndpointAuthMethod = ClientAuthenticationMethods.ClientSecretPost,
AllowedGrantTypes = new[] { GrantTypes.AuthorizationCode },
ClientType = ClientType.Confidential,
OfflineAccessAllowed = true,
PkceRequired = true,
RedirectUris = new[] { new Uri("https://localhost:5003/signin-oidc", UriKind.Absolute) },
PostLogoutRedirectUris = new[] { new Uri("https://localhost:5003/signout-callback-oidc", UriKind.Absolute) },
}
};
// ******************** FIN ********************
// L'URL suivante mène à l'action Login du contrôleur Auth
options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);
// La ligne suivante génère une nouvelle clé pour la signature des jetons. Remplacez-la si vous souhaitez utiliser vos propres clés.
options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});
Examinons ce code en détail. Nous enregistrons un client avec l’identifiant bff_sample
et le jeton secret secret
(en le stockant sous la forme d’un hash SHA512), en indiquant que l’acquisition du token utilisera l’authentification du client avec le jeton secret envoyé dans un message POST (ClientAuthenticationMethods.ClientSecretPost
). AllowedGrantTypes
spécifie que le client n’est autorisé qu’à utiliser le Flux d’autorisation Code. ClientType
définit le client comme confidentiel, ce qui signifie qu’il peut stocker en toute sécurité son jeton secret. OfflineAccessAllowed
permet au client d’utiliser les jetons de rafraîchissement. PkceRequired
impose l’utilisation de PKCE lors de l’authentification. RedirectUris
et PostLogoutRedirectUris
contiennent respectivement des listes d’URL autorisées pour la redirection après l’authentification et à la fin de la session.
Pour toute autre serveur OpenID Connect, les paramètres seront similaires, avec des différences uniquement dans la manière dont ils sont configurés.
Implémenter l’API de base BFF
Précédemment, nous avons mentionné que l’utilisation du package Microsoft.AspNetCore.Authentication.OpenIdConnect
ajoute automatiquement l’implémentation de l’extrémité de Connexion au notre application de démonstration. Maintenant, il est temps d’implémenter la partie restante de l’API BFF. Nous utiliserons un contrôleur ASP.NET MVC pour ces extrémités supplémentaires. Commençons par ajouter un dossier Controllers
et un fichier BffController.cs
dans le projet BffSample
avec le code suivant à l’intérieur :
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BffSample.Controllers;
[ApiController]
[Route("[controller]")]
public class BffController : Controller
{
public const string CorsPolicyName = "Bff";
[HttpGet("check_session")]
[EnableCors(CorsPolicyName)]
public ActionResult> CheckSession()
{
// Retourne 401 Non autorisé pour forcer le redirectionnement du SPA vers l'extrémité de connexion
if (User.Identity?.IsAuthenticated != true)
return Unauthorized();
return User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
}
[HttpGet("login")]
public ActionResult> Login()
{
// Logique pour démarrer le flux d'autorisation
return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
}
[HttpPost("logout")]
public IActionResult Logout()
{
// Logique pour gérer la déconnexion de l'utilisateur
return SignOut();
}
}
Divisons en détail ce code de classe :
- L’attribut
[Route("[controller]")]
définit la route de base pour toutes les actions dans le contrôleur. Dans ce cas, la route correspondra au nom du contrôleur, ce qui signifie que toutes les chemins vers nos méthodes API commenceront par/bff/
. - La constante
CorsPolicyName = "Bff"
définit le nom de la politique CORS (Cross-Origin Resource Sharing) à utiliser dans les attributs de méthode. Nous y renverrons plus tard. - Les trois méthodes
CheckSession
,Login
etLogout
mettent en œuvre les fonctionnalités nécessaires du BFF décrites ci-dessus. Elles gèrent les demandes GET à/bff/check_session
,/bff/login
et les demandes POST à/bff/logout
respectivement. - La méthode
CheckSession
vérifie l’état d’authentification de l’utilisateur. Si l’utilisateur n’est pas authentifié, elle retourne un code401 Non autorisé
, qui devrait forcer le SPA à rediriger vers l’extrémité d’authentification. Si l’authentification est réussie, la méthode retourne un ensemble de revendications et leurs valeurs. Cette méthode inclut également une liaison de politique CORS avec le nomCorsPolicyName
car la méthode peut être cross-domain et contenir des cookies utilisés pour l’authentification de l’utilisateur. - La méthode
Login
est appelée par le SPA si l’appel précédent deCheckSession
retourne401 Non autorisé
. Elle vérifie que l’utilisateur n’est toujours pas authentifié et initie le processus configuréChallenge
, qui entraînera une redirection vers le serveur OpenID Connect, une authentification de l’utilisateur à l’aide du Flux d’autorisation Code et de PKCE, et l’émission d’une variable de session. Après cela, le contrôle retourne à la racine de notre application"~/"
, qui déclenchera le SPA pour recharger et démarrer avec un utilisateur authentifié. - La méthode
Logout
est également appelée par le SPA mais termine la session d’authentification en cours. Elle supprime les variables de session d’authentification émises par la partie serveur deBffSample
et appelle également l’extrémité Fin de Session sur le côté serveur OpenID Connect.
Configurer CORS pour BFF
Comme mentionné précédemment, la méthode CheckSession
est destinée à des appels asynchrones depuis le SPA (généralement en utilisant l’API Fetch). Le fonctionnement correct de cette méthode dépend de la capacité d’envoyer des variables de session d’authentification depuis le navigateur. Si le SPA est chargé à partir d’un hôte Web statique distinct, comme un CDN ou un serveur de développement exécutant sur un port différent, ce appel devient cross-domain. Cela implique la nécessité de configurer une stratégie CORS, sans laquelle le SPA ne pourra pas appeler cette méthode.
Nous avons déjà indiqué dans le code du contrôleur dans le fichier Controllers\BffController.cs
que la stratégie CORS nommée CorsPolicyName = "Bff"
doit être utilisée. Il est temps de configurer les paramètres de cette stratégie pour résoudre notre problème. Retournons au fichier BffSample/Program.cs
et ajoutons les blocs de code suivants :
// ******************* DÉBUT *******************
using BffSample.Controllers;
// ******************** FIN ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ...
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* DÉBUT *******************
builder.Services.AddCors(
options => options.AddPolicy(
BffController.CorsPolicyName,
policyBuilder =>
{
var allowedOrigins = configuration.GetSection("CorsSettings:AllowedOrigins").Get();
if (allowedOrigins is { Length: > 0 })
policyBuilder.WithOrigins(allowedOrigins);
policyBuilder
.WithMethods(HttpMethods.Get)
.AllowCredentials();
}));
// ******************** FIN ********************
var app = builder.Build();
Ce code permet aux méthodes de la politique CORS d’être appellées à partir des SPAs chargés à partir de sources spécifiées dans la configuration sous forme d’un tableau de chaînes de caractères CorsSettings:AllowedOrigins
, en utilisant la méthode GET
et permet le transfert de cookies dans cette appel. De plus, veillez à placer l’appel à app.UseCors(...)
juste avant app.UseAuthentication()
:
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* DÉBUT *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** FIN ********************
app.UseAuthentication();
app.UseAuthorization();
Pour assurer que la politique CORS fonctionne correctement, ajoutez le paramètre correspondant au fichier de configuration BffSample\appsettings.Development.json
:
{
// ******************* DÉBUT *******************
"CorsSettings": {
"AllowedOrigins": [ "https://localhost:3000" ]
},
// ******************** FIN ********************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
Dans notre exemple, l’adresse https://localhost:3000
est l’endroit où le serveur de développement avec le SPA React est lancé en utilisant la commande npm run dev
. Vous pouvez trouver cette adresse en ouvrant le fichier BffSample.csproj
et en trouvant la valeur du paramètre SpaProxyServerUrl
. Dans une application réelle, la politique CORS pourrait inclure l’adresse de votre CDN (Content Delivery Network) ou un service similaire. Il est important de se rappeler que si votre SPA est chargé à partir d’une adresse et d’un port différent de celui fournissant l’API BFF, vous devez ajouter cette adresse à la configuration de la politique CORS.
Implémenter l’authentification via BFF dans une application React
Nous avons implémenté l’API BFF côté serveur. Maintenant, il est temps de nous concentrer sur l’SPA React et d’ajouter les fonctionnalités correspondantes pour appeler cette API. Commençons par naviguer vers le dossier BffSample\ClientApp\src\
, créons un dossier components
, et ajoutons un fichier Bff.tsx
avec le contenu suivant :
import React, { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react';
// Définir la structure du contexte BFF
interface BffContextProps {
user: any;
fetchBff: (endpoint: string, options?: RequestInit) => Promise;
checkSession: () => Promise;
login: () => void;
logout: () => Promise;
}
// Créer un contexte pour BFF pour partager l'état et les fonctions à travers l'application
const BffContext = createContext({
user: null,
fetchBff: async () => new Response(),
checkSession: async () => {},
login: () => {},
logout: async () => {}
});
interface BffProviderProps {
baseUrl: string;
children: ReactNode;
}
export const BffProvider: FC = ({ baseUrl, children }) => {
const [user, setUser] = useState(null);
// Normaliser l'URL de base en enlevant le slash final pour éviter les URL incohérentes
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
try {
// La fonction fetch inclut des identifiants pour gérer les cookies, nécessaires pour l'authentification
return await fetch(`${baseUrl}/${endpoint}`, {
credentials: 'include',
...options
});
} catch (error) {
console.error(`Error during ${endpoint} call:`, error);
throw error;
}
};
// La fonction login redirige vers la page de connexion lorsque l'utilisateur doit s'authentifier
const login = (): void => {
window.location.replace(`${baseUrl}/login`);
};
// La fonction checkSession est responsable de la vérification de la session de l'utilisateur à l'affichage initial
const checkSession = async (): Promise => {
const response = await fetchBff('check_session');
if (response.ok) {
// Si la session est valide, mettre à jour l'état de l'utilisateur avec les données de revendication reçues
setUser(await response.json());
} else if (response.status === 401) {
// Si l'utilisateur n'est pas authentifié, le rediriger vers la page de connexion
login();
} else {
console.error('Unexpected response from checking session:', response);
}
};
// Fonction pour déconnecter l'utilisateur
const logout = async (): Promise => {
const response = await fetchBff('logout', { method: 'POST' });
if (response.ok) {
// Rediriger vers la page d'accueil après une déconnexion réussie
window.location.replace('/');
} else {
console.error('Logout failed:', response);
}
};
// useEffect est utilisé pour exécuter la fonction checkSession une fois que le composant est monté
// Cela assure que la session est vérifiée immédiatement lors du chargement de l'application
useEffect(() => { checkSession(); }, []);
return (
// Fournir le contexte BFF avec les valeurs et fonctions pertinentes pour être utilisées à travers l'application
{children}
);
};
// Hook personnalisé pour utiliser facilement le contexte BFF dans d'autres composants
export const useBff = (): BffContextProps => useContext(BffContext);
// Exporter HOC pour fournir accès au contexte BFF
export const withBff = (Component: React.ComponentType) => (props: any) =>
{context => }
;
Ce fichier exporte :
- Le composant
BffProvider
, qui crée un contexte pour BFF et fournit les fonctions et l’état liés à l’authentification et à la gestion de la session pour l’application entière. - La fonctionnalité personnalisée
useBff()
retourne un objet contenant l’état actuel de l’utilisateur et des fonctions pour travailler avec le BFF :checkSession
,login
, etlogout
. Elle est conçue pour être utilisée dans les composants React fonctionnels. - La composante de plus haut niveau (HOC)
withBff
pour l’utilisation dans les composants React basés sur les classes.
Ensuite, créez un composant UserClaims
, qui affiche les revendications de l’utilisateur actuel une fois l’authentification réussie. Créez un fichier UserClaims.tsx
dans le dossier BffSample\ClientApp\src\components
avec le contenu suivant :
import React from 'react';
import { useBff } from './Bff';
export const UserClaims: React.FC = () => {
const { user } = useBff();
if (!user)
return <div>Checking user session...</div>;
return (
<>
<h2>User Claims</h2>
{Object.entries(user).map(([claim, value]) => (
<div key={claim}>
<strong>{claim}</strong>: {String(value)}
</div>
))}
</>
);
};
Ce code vérifie l’existence d’un utilisateur authentifié en utilisant l’hook useBff()
et affiche les revendications de l’utilisateur sous forme de liste si l’utilisateur est authentifié. Si les données de l’utilisateur ne sont pas encore disponibles, il affiche le texte Checking user session...
.
Maintenant, passez au fichier BffSample\ClientApp\src\App.tsx
. Remplacez son contenu par le code nécessaire. Importez BffProvider
de components/Bff.tsx
et UserClaims
de components/UserClaims.tsx
, et insérez le code du composant principal :
import { BffProvider, useBff } from './components/Bff';
import { UserClaims } from './components/UserClaims';
const LogoutButton: React.FC = () => {
const { logout } = useBff();
return (
<button className="logout-button" onClick={logout}>
Logout
</button>
);
};
const App: React.FC = () => (
<BffProvider baseUrl="https://localhost:5003/bff">
<div className="card">
<UserClaims/>
</div>
<div className="card">
<LogoutButton />
</div>
</BffProvider>
);
export default App;
Dans cet exemple, le paramètre baseUrl
spécifie l’URL de base de notre API BFF https://localhost:5003/bff
. Cette simplification est intentionnelle et réalisée pour la simplicité. Dans une application réelle, vous devriez fournir ce paramètre de manière dynamique plutôt que de le coder à la main. Il existe diverses manières de réaliser cela, mais en discutant de celles-ci dépasserait la portée de cet article.
Le bouton Déconnexion
permet à l’utilisateur de se déconnecter. Il appelle la fonction logout
disponible via le hook useBff
et redirige le navigateur de l’utilisateur vers l’endroit /bff/logout
, qui termine la session de l’utilisateur côté serveur.
A ce stade, vous pouvez maintenant exécuter l’application BffSample
en même temps que l’application OpenIDProviderApp
et tester ses fonctionnalités. Vous pouvez utiliser la commande dotnet run -lp https
dans chaque projet ou votre IDE préféré pour les démarrer. Les deux applications doivent être exécutées simultanément.
Après cela, ouvrez votre navigateur et naviguez vers https://localhost:5003
. Si tout est correctement configuré, l’SPA se chargera et appellera /bff/check_session
. L’endroit /check_session
retournera une réponse 401, poussant l’SPA à rediriger le navigateur vers /bff/login
, qui initiera alors l’authentification sur le serveur via le Flux d’autorisation OpenID Connect utilisant PKCE. Vous pouvez observer cette séquence de demandes enouvrant la Console de développement de votre navigateur et en allant sur l’onglet Réseau. Après avoir réussi à entrer les informations d’identification de l’utilisateur ([email protected]
, Jd!2024$3cur3
), le contrôle retournera à l’SPA et vous verrez les revendications de l’utilisateur authentifié dans le navigateur :
sub: 1234567890
sid: V14fb1VQbAFG6JXTYQp3D3Vpa8klMLcK34RpfOvRyxQ
auth_time: 1717852776
name: John Doe
email: [email protected]
De plus, cliquer sur le bouton Déconnexion
redirigera le navigateur vers /bff/logout
, qui déconnectera l’utilisateur et vous verrez à nouveau la page de connexion avec un avertissement pour entrer votre nom d’utilisateur et votre mot de passe.
Si vous rencontrez des erreurs, vous pouvez comparer votre code avec notre dépôt GitHub Abblix/Oidc.Server.GettingStarted, qui contient ce cas d’utilisation et d’autres exemples prêts à être exécutés.
Résolution des problèmes de confiance de certificats HTTPS
Lorsque vous testez localement des applications web configurées pour fonctionner sur HTTPS, vous pouvez rencontrer des avertissements du navigateur indiquant que le certificat SSL n’est pas de confiance. Cet issue se produit parce que les certificats de développement utilisés par ASP.NET Core ne sont pas émis par une Autorité de Certification (CA) reconnue mais sont signés par vous-même ou n’existent pas du tout dans le système. Ces avertissements peuvent être éliminés en exécutant la commande suivante une fois :
dotnet dev-certs https --trust
Cette commande génère un certificat auto-signé pour localhost
et l’installe dans votre système pour qu’il le considère comme de confiance. Ce certificat sera utilisé par ASP.NET Core pour exécuter des applications web localement. Après l’exécution de cette commande, redémarrez votre navigateur pour que les modifications prennent effet.
Note spéciale pour les utilisateurs de Chrome : même après l’installation du certificat de développement comme étant de confiance, certaines versions de Chrome peuvent toujours restreindre l’accès aux sites de localhost
pour des raisons de sécurité. Si vous rencontrez une erreur indiquant que votre connexion n’est pas sûre et que l’accès à localhost
est bloqué par Chrome, vous pouvez ignorer cela comme suit :
- Cliquez n’importe où sur la page d’erreur et tapez
thisisunsafe
oubadidea
, selon la version de Chrome que vous utilisez. Ces séquences de touches agissent comme des commandes de bypass dans Chrome, vous permettant de continuer vers le site delocalhost
.
Il est important d’utiliser ces méthodes de bypass uniquement dans des scénarios de développement, car elles peuvent poser des risques de sécurité réels.
Appeler des API tierces via BFF
Nous avons réussi à mettre en œuvre l’authentification dans notre application BffSample
. Maintenant, passons à l’appel d’une API tierce qui nécessite un jeton d’accès.
Imaginez que nous avons un service séparé qui fournit les données nécessaires, telles que les prévisions météorologiques, et que l’accès à celui-ci est accordé uniquement avec un jeton d’accès. Le rôle de la partie serveur de BffSample
sera d’agir en tant que proxy inverse, c’est-à-dire qu’il acceptera et authentifiera la requête de données pour le SPA, ajoutera le jeton d’accès à celle-ci, forwardera cette requête au service météorologique, puis retournera la réponse du service au SPA.
Création du service ApiSample
Avant de montrer l’appel d’API distant via le BFF, nous devons créer une application qui servira de cette API dans notre exemple.
Pour créer l’application, nous utiliserons un modèle fourni par .NET. Naviguez vers le dossier contenant les projets OpenIDProviderApp
et BffSample
, et exécutez le commande suivante pour créer l’application ApiSample
:
dotnet new webapi -n ApiSample
Cette application ASP.NET Core Minimal API fournit un unique point d’entrée avec l’URL /weatherforecast
qui fournit des données météorologiques au format JSON.
Premièrement, changez le numéro de port assigné aléatoirement que l’application ApiSample
utilise localement pour un port fixe, le 5004. Comme mentionné plus tôt, cette étape n’est pas obligatoire, mais elle simplifie notre configuration. Pour ce faire, ouvrez le fichier ApiSample\Properties\launchSettings.json
, trouvez la profile nommée https
et changez la valeur de la propriété applicationUrl
à https://localhost:5004
.
Maintenant, essayons de rendre l’API météo accessible uniquement avec un jeton d’accès. Naviguez vers le dossier du projet ApiSample
et ajoutez le paquet NuGet pour l’authentification par jeton JWT Bearer :
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Configurez le schéma d’authentification et la politique d’autorisation nommée WeatherApi
dans le fichier ApiSample\Program.cs
:
// ******************* DEBUT *******************
using System.Security.Claims;
// ******************** FIN ********************
var builder = WebApplication.CreateBuilder(args);
// Ajoutez des services au conteneur.
// En savoir plus sur la configuration de Swagger/OpenAPI sur https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* DEBUT *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthentication()
.AddJwtBearer(options => configuration.Bind("JwtBearerAuthentication", options));
const string policyName = "WeatherApi";
builder.Services.AddAuthorization(
options => options.AddPolicy(policyName, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(context =>
{
var scopeValue = context.User.FindFirstValue("scope");
if (string.IsNullOrEmpty(scopeValue))
return false;
var scope = scopeValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return scope.Contains("weather", StringComparer.Ordinal);
});
}));
// ******************** FIN ********************
var app = builder.Build();
Ce bloc de code configure l’authentification en lisant la configuration depuis les paramètres de l’application, inclut l’autorisation en utilisant JWT (JSON Web Tokens), et configure une politique d’autorisation nommée WeatherApi
. La politique d’autorisation WeatherApi
définit les exigences suivantes :
policy.RequireAuthenticatedUser()
: Fait en sorte que seuls les utilisateurs authentifiés puissent accéder aux ressources protégées.policy.RequireAssertion(context => ...)
: L’utilisateur doit avoir une déclarationscope
qui inclut la valeurweather
. Comme la déclarationscope
peut contenir plusieurs valeurs séparées par des espaces selon RFC 8693, la valeur réellescope
est divisée en parties individuelles, et l’arrêt en résultat est vérifié pour contenir la valeur requiseweather
.
Ensemble, ces conditions assurent que seuls les utilisateurs authentifiés avec un jeton d’accès autorisé pour le weather
scope peuvent appeler l’extrémité protégée par cette politique.
Nous devons appliquer cette politique à l’extrémité /weatherforecast
. Ajoutez l’appel à RequireAuthorization()
comme illustré ci-dessous:
app.MapGet("/weatherforecast", () =>
{
// ...
})
.WithName("GetWeatherForecast")
// ******************* DÉBUT *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************** FIN ********************
Ajoutez les paramètres de configuration nécessaires pour le schéma d’authentification dans le fichier appsettings.Development.json
de l’application ApiSample
:
{
// ******************* DÉBUT *******************
"JwtBearerAuthentication": {
"Authority": "https://localhost:5001",
"MapInboundClaims": false,
"TokenValidationParameters": {
"ValidTypes": [ "at+jwt" ],
"ValidAudience": "https://localhost:5004",
"ValidIssuer": "https://localhost:5001"
}
},
// ******************** FIN ********************
"Logging": {
"LogLevel": {
"Default": "Information",
Examinez chaque paramètre en détail:
Authority
: C’est l’URL pointant vers le serveur d’autorisation OpenID Connect qui émet des jetons JWT. Le fournisseur d’authentification configuré dans l’applicationApiSample
utilisera cette URL pour obtenir les informations nécessaires pour vérifier les jetons, telles que les clés de signature.MapInboundClaims
: Ce paramètre contrôle comment les revendications entrantes du jeton JWT sont mappées aux revendications internes dans ASP.NET Core. Il est réglé surfalse
, ce qui signifie que les revendications utiliseront leurs noms d’origine du JWT.TokenValidationParameters
:ValidTypes
: Réglé surat+jwt
, ce qui selon RFC 9068 2.1 indique un jeton d’accès au format JWT.ValidAudience
: Spécifie que l’application acceptera les jetons émis pour le clienthttps://localhost:5004
.ValidIssuer
: Spécifie que l’application acceptera les jetons émis par le serveurhttps://localhost:5001
.
Configuration supplémentaire de OpenIDProviderApp
La combinaison du service d’authentification OpenIDProviderApp
et de l’application cliente BffSample
fonctionne bien pour fournir l’authentification des utilisateurs. Cependant, pour permettre les appels à une API distante, nous devons enregistrer l’application ApiSample
en tant que ressource auprès de OpenIDProviderApp
. Dans notre exemple, nous utilisons l’Abblix OIDC Server
, qui supporte RFC 8707: Resource Indicators for OAuth 2.0. Par conséquent, nous enregistrerons l’application ApiSample
en tant que ressource avec l’échelle weather
. Si vous utilisez un autre serveur OpenID Connect qui ne supporte pas les Indicateurs de ressource, il est encore recommandé d’enregistrer un échelon unique pour cette API distante (comme weather
dans notre exemple).
Ajoutez le code suivant au fichier OpenIDProviderApp\Program.cs
:
// Enregistrez et configurez l'Abblix OIDC Server
builder.Services.AddOidcServices(options => {
// ******************* DÉBUT *******************
options.Resources =
[
new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
];
// ******************** FIN ********************
options.Clients = new[] {
new ClientInfo("bff_sample") {
Dans cet exemple, nous enregistons l’application ApiSample
, en spécifiant son adresse de base https://localhost:5004
en tant que ressource et en définissant un scope spécifique nommé weather
. Dans les applications réelles, en particulier celles avec des API complexes composées de nombreux points de terminaison, il est conseillé de définir des scopes distincts pour chaque point de terminaison individuel ou groupe de points de terminaison liés. Cette approche permet un contrôle d’accès plus précis et offre de la flexibilité dans la gestion des droits d’accès. Par exemple, vous pouvez créer des scopes distincts pour différentes opérations, modules d’application ou niveaux d’accès des utilisateurs, ce qui permet un contrôle plus granulaire sur qui peut accéder à des parties spécifiques de votre API.
Elaboration de BffSample pour le proxying des requêtes vers une API distante
L’application cliente BffSample
doit maintenant faire plus que de demander un jeton d’accès pour ApiSample
. Elle doit également gérer les requêtes de l’SPA vers l’API distante. Cela implique d’ajouter le jeton d’accès obtenu depuis le service OpenIDProviderApp
à ces requêtes, de les转发 vers le serveur distant et de retourner les réponses du serveur vers l’SPA. En essence, BffSample
doit fonctionner en tant que serveur reverse-proxy.
Plutôt que de implémenter manuellement le proxying des requêtes dans notre application cliente, nous utiliserons YARP (Yet Another Reverse Proxy), un produit prêté à l’emploi développé par Microsoft. YARP est un serveur reverse-proxy écrit en .NET et disponible en tant que package NuGet.
Pour utiliser YARP dans l’application BffSample
, ajoutez d’abord le package NuGet :
dotnet add package Yarp.ReverseProxy
Ensuite, ajoutez les espaces de noms suivants au début du fichier BffSample\Program.cs
:
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net.Http.Headers;
using Yarp.ReverseProxy.Transforms;
Avant l’appel var app = builder.Build();
, ajoutez le code :
builder.Services.AddHttpForwarder();
Et entre les appels à app.MapControllerRoute()
et app.MapFallbackToFile()
:
app.MapForwarder(
"/bff/{**catch-all}",
configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
builderContext =>
{
// Supprimez le préfixe "/bff" de l'itinéraire de la requête
builderContext.AddPathRemovePrefix("/bff");
builderContext.AddRequestTransform(async transformContext =>
{
// Obtenez l'accès token reçu plus tôt pendant le processus d'authentification
var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
// Ajoutez un en-tête avec l'accès token à la requête du proxy
transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
});
}).RequireAuthorization();
Voyons le détail de ce code :
builder.Services.AddHttpForwarder()
enregistre les services nécessaires de YARP dans le conteneur de composants de l’Istance.app.MapForwarder
configure l’acheminement de la requête vers un autre serveur ou point de terminaison."/bff/{**catch-all}"
est le schéma d’itinéraire des chemins que le proxy inverse répondra à. Toutes les requêtes commençant par/bff/
seront traitées par YARP.{**catch-all}
est utilisé pour capturer toutes les parties restantes de l’URL après/bff/
.configuration.GetValue<string>("OpenIdConnect:Resource")
utilise la configuration de l’application pour obtenir la valeur de la sectionOpenIdConnect:Resource
. Cette valeur spécifie l’adresse de ressource vers laquelle les requêtes seront détournées. Dans notre exemple, cette valeur serahttps://localhost:5004
– l’URL de base où l’applicationApiSample
fonctionne.builderContext => ...
ajoute les transformations nécessaires queARP effectuera sur chaque requête entrante du SPA. Dans notre cas, il y aura deux transformations :builderContext.AddPathRemovePrefix("/bff")
supprime le préfixe/bff
du chemin de requête originale.builderContext.AddRequestTransform(async transformContext => ...)
ajoute un en-tête HTTPAuthorization
à la requête contenant le jeton d’accès précédemment obtenu lors de l’authentification. Ainsi, les requêtes du SPA à l’API distante seront authentifiées en utilisant le jeton d, même si le SPA lui-même n’y a pas accès.
.RequireAuthorization()
spécifie que l’autorisation est requise pour toutes les requ transférées. Seuls les utilisateurs autorisés pourront accéder à la route/bff/{**catch-all}
.
Pour demander un jeton d’accès pour le ressource https://localhost:5004
pendant l’authentification, ajoutez le paramètre Resource
avec la valeur https://localhost:5004
à la configuration OpenIdConnect
dans le fichier BffSample/appsettings.Development.json
:
"OpenIdConnect": {
// ******************* DÉBUT *******************
"Resource": "https://localhost:5004",
// ******************** FIN ********************
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
De plus, ajoutez un autre élément weather
au tableau scope
dans le fichier BffSample/appsettings.json
:
{
"OpenIdConnect": {
// ...
// ******************* DÉBUT *******************
"Scope": ["openid", "profile", "email", "weather"],
// ******************** FIN ********************
// ...
}
}
Notes : Dans un projet réel, il est nécessaire de surveiller l’expiration du jeton d’accès. Lorsque le token est sur le point d’expirer, vous devez soit demander un nouveau token à l’avance en utilisant un jeton de rafraîchissement de l’authentification, soit gérer l’erreur d’accès refusé de l’API distante en obtenant un nouveau token et réessayer la demande initiale. Pour la concision, nous avons délibérément omis ce aspect dans cet article.
Requête de l’API Météo via BFF dans l’application SPA
Le backend est maintenant prêt. Nous avons l’application ApiSample
, qui implémente une API avec une autorisation basée sur le token, et l’application BffSample
, qui inclut un serveur reverse-proxy intégré pour fournir un accès sécurisé à cette API. La dernière étape est d’ajouter la fonctionnalité de demander cette API et d’afficher les données obtenues dans l’application SPA React.
Ajoutez le fichier WeatherForecast.tsx
dans BffSample\ClientApp\src\components
avec le contenu suivant :
import React, { useEffect, useState } from 'react';
import { useBff } from './Bff';
interface Forecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
interface State {
forecasts: Forecast[];
loading: boolean;
}
export const WeatherForecast: React.FC = () => {
const { fetchBff } = useBff();
const [state, setState] = useState<State>({ forecasts: [], loading: true });
const { forecasts, loading } = state;
useEffect(() => {
fetchBff('weatherforecast')
.then(response => response.json())
.then(data => setState({ forecasts: data, loading: false }));
}, [fetchBff]);
const contents = loading
? <p><em>Loading...</em></p>
: (
<table className="table table-striped" aria-labelledby="tableLabel">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{forecasts.map((forecast, index) => (
<tr key={index}>
<td>{forecast.date}</td>
<td align="center">{forecast.temperatureC}</td>
<td align="center">{forecast.temperatureF}</td>
<td>{forecast.summary}</td>
</tr>
))}
</tbody>
</table>
);
return (
<div>
<h2 id="tableLabel">Weather forecast</h2>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
};
Parseons ce code :
- L’interface
Forecast
définit la structure des données de prévision météorologique, qui inclut la date, la température en degrés Celsius et Fahrenheit, et un résumé de la météo. L’interfaceState
décrit la structure de l’état du composant, composé d’un tableau des prévisions météorologiques et d’un drapeau de chargement. - Le composant
WeatherForecast
récupère la fonctionfetchBff
de l’hookuseBff
et l’utilise pour récupérer les données météorologiques du serveur. L’état du composant est géré à l’aide de l’hookuseState
, initialisé avec un tableau vide de prévisions et un drapeau de chargement défini à vrai. - L’hook
useEffect
déclenche la fonctionfetchBff
lorsque le composant est monté, en récupérant les données de prévision météorologique du serveur à l’endroit/bff/weatherforecast
. Une fois que la réponse du serveur est reçue et convertie en JSON, les données sont stockées dans l’état du composant (à traverssetState
), et le drapeau de chargement est mis à faux. - Selon la valeur du drapeau de chargement, le composant affiche soit un message « Chargement… » soit affiche une table avec les données de prévision météorologique. La table comprend des colonnes pour la date, la température en degrés Celsius et Fahrenheit, et un résumé de la météo pour chaque prévision.
Maintenant, ajoutez le composant WeatherForecast
à BffSample\ClientApp\src\App.tsx
:
// ******************* DÉBUT *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************* FIN *******************
// ...
// ******************* DÉBUT *******************
// ******************* FIN *******************
Exécution et Tests
Si tout a été fait correctement, vous pouvez maintenant démarrer les trois projets que nous avons créés. Utilisez la commande de console dotnet run -lp https
pour chacune des applications afin de les exécuter avec HTTPS.
Après avoir lancé toutes les trois applications, ouvrez l’application BffSample
dans votre navigateur (https://localhost:5003) et vous identifier en utilisant les informations de connexion [email protected]
et Jd!2024$3cur3
. Après une identification réussie, vous devriez voir la liste des revendications reçues de l’authentification serveur, comme vu précédemment. En dessous de cela, vous verrez également le prévisionnel météorologique.
Le prévisionnel météorologique est fourni par l’application distincte ApiSample
, qui utilise un jeton d’accès émis par le service d’authentification OpenIDProviderApp
. Voir le prévisionnel météorologique dans la fenêtre de l’application BffSample
indique que notre SPA a réussi à appeler le backend de BffSample
, qui a ensuite fait de la mise en cache vers ApiSample
en ajoutant le jeton d’accès. ApiSample
a authentifié la demande et a répondu avec un fichier JSON contenant le prévisionnel météorologique.
La solution complète est disponible sur GitHub.
Si vous rencontrez des problèmes ou des erreurs lors de la mise en œuvre des projets de test, vous pouvez vous référer à la solution complète disponible dans le dépôt GitHub. Il suffit de cloner le dépôt Abblix/Oidc.Server.GettingStarted pour accéder aux projets pleinement implémentés décrits dans cet article. Ce ressource sert à la fois de outil de dépannage et de point de départ solide pour créer vos propres projets.
Conclusion
L’évolution des protocoles d’authentification tels que OAuth 2.0 et OpenID Connect reflète les tendances plus larges en matière de sécurité Web et des capacités du navigateur. Le passage de l’Implicit Flow, méthode périmée, vers des approches plus sécurisées, telles que le Flow d’Autorisation Code avec PKCE, a considérablement amélioré la sécurité. Cependant, les vulnérabilités inhérentes à l’opération dans des environnements non contrôlés font de la sécurisation des modernes SPA une tâche difficile. Le stockage des tokens exclusivement sur le backend et l’adoption du pattern Backend-For-Frontend (BFF) sont des stratégies efficaces pour atténuer les risques et assurer une protection robuste des données utilisateurs.
Les développeurs doivent rester vigilants en traitant constamment le paysage des menaces en演变uant en misant en œuvre de nouvelles méthodes d’authentification et des approches architecturales à jour. Cette approche proactive est essentielle pour construire des applications Web sécurisées et fiables. Dans cet article, nous avons exploré et mis en œuvre une approche moderne pour intégrer OpenID Connect, BFF et SPA en utilisant la stack technique populaire .NET et React. Cette approche peut servir de fondation solide pour vos projets futurs.
Au fil de l’avenir, l’évolution continue de la sécurité Web exigerait une plus grande innovation en matière d’authentification et de schémas architecturaux. Nous vous encourageons à explorer notre dépôt GitHub, à contribuer au développement de solutions modernes d’authentification et à rester impliqué dans les progrès en cours. Merci pour votre attention !
Source:
https://dzone.com/articles/modern-authentication-on-dotnet