Objectif
Le but de cet article est de montrer comment l’IAM préparée sur le précédent article va être utilisée par l’application cliente pour son authentification / identification. Dans cet article, nous n’utiliserons pas encore l’identification pour réaliser des autorisations dans le client, et nous ne propagerons pas l’authentification sur le serveur. Ces deux sujets seront traités dans d’autres articles à venir. Pour l’instant, le but est juste de montrer comment authentifier proprement un client Blazor sur une source OpenID Connect.
Vous êtes certainement déjà tombés sur des articles comme celui-ci, mettant en garde sur la mauvaise utilisation des tokens JWT, et il faut effectivement faire attention à ne pas créer des failles de sécurité. C’est pour cette raison que, dans l’application exemple, nous collerons au maximum à l’utilisation la plus standard possible des librairies .NET. C’est ce qui nous permettra en particulier d’éviter le maximum de pièges de sécurité, car les implémentations fournies sont sécurisées par défaut. Par exemple, le type de flux utilisé sera le mode "code grant" d’OpenID Connect, qui est plus sécurisé que le mode "implicit grant", désormais abandonné par la vesion OAuth 2.1. Et pour ce qui est des tokens, l’implémentation .NET par défaut gère correctement les id token, access token et refresh token, ce qui donne également une bonne assurance d’évolutivité, par exemple vers la possibilité de révocation d’une session authentifiée.
Création du squelette d’application standard
Toujours pour des raisons de standardisation maximum, nous partirons du modèle Application WebAssembly Blazor de Visual Studio (version 2022 utilisée pour les articles) :
Si vous souhaitez suivre directement les exercices, vous pouvez nommer l’application TestOIDCBlazorWASM, de façon à pouvoir copier directement les ensembles de code faisant référence aux namespaces, sans avoir à les modifier :
Afin de montrer précisément tous les ajouts nécessaires à la bonne gestion de l’authentification et d’expliquer point par point toutes les classes et fonctionnalités pour ce faire, nous partirons, même si cela peut paraître plus de travail, d’une application sans gestion de l’authentification. Et comme le but est d’avoir une application la plus réaliste possible, nous utiliserons le mode HTTPS :
Ajout des composants Blazor à utiliser pour l’authentification
La première étape est d’ajouter le package nécessaire à la gestion de l’authentification dans une application Blazor WASM, à savoir Microsoft.AspNetCore.Components.WebAssembly.Authentication
. Pour cela, on va dans la gestion des packages du projet Client :
Et on installe cette librairie qui, au moment de l’écriture de cet article, était en 6.0.8 :
Ensuite, nous allons créer trois composants Razor, dont le code est récupéré sur les templates avec authentification proposés par Microsoft, mais que nous reprendrons ici manuellement :
Le premier sera placé dans Pages
, s’appellera Authentication.razor
et contiendra le code suivant, nécessaire pour la gestion des routes d’authentification :
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@code{
[Parameter] public string? Action { get; set; }
}
Attention à ne pas modifier la route exposée, en tout cas sans ajuster ce qui avait été fait dans le précédent article sur le paramétrage de l’IAM Keycloak, car nous avions déclaré les sous-routes login-callback
et logout-callback
dans /authentication
comme les URLs valides pour l’IAM, et il faut que la correspondance soit gardée sinon l’IAM refusera ces adresses en redirection.
Le second composant sera placé dans Shared
et nommé LoginDisplay.razor
, avec ce contenu :
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager
<AuthorizeView>
<Authorized>
<a href="authentication/profile">Hello, @context.User.Identity?.Name !</a>
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
</Authorized>
<NotAuthorized>
<a href="authentication/register">Register</a>
<a href="authentication/login">Log in</a>
</NotAuthorized>
</AuthorizeView>
@code{
private async Task BeginSignOut(MouseEventArgs args)
{
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}
Ce composant est celui qui gère l’affichage des commandes d’authentification. Tant que l’utilisateur n’est pas identifié, il affiche le lien pour s’enregistrer comme utilisateur (attention, ça ne veut pas dire que l’IAM supporte cette fonctionnalité, et si ce n’est pas le cas, on aura une information en ce sens) et le lien pour se connecter. Dans le cas inverse, l’affichage montrera le contenu du nom fourni par l’instance d’identité générée par .NET à partir des claims d’identification reçus par le ticket JWT, ainsi que le lien pour se déconnecter (d’où l’importance, dans l’article précédent, de bien déclarer aussi l’URL en logout-callback
comme valide).
Le troisième composant est également dans Shared
, et s’appelle RedirectToLogin.razor
. Il contient le code ci-dessous :
@inject NavigationManager Navigation
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
}
}
Comme le nom l’indique, ce composant se charge uniquement de la redirection vers la page de login, en passant en paramètre l’URL sur laquelle rediriger l’utilisateur si la connexion a bien été réalisée (en général, la page d’où l’utilisateur vient, et qui l’a renvoyé sur la page de login comme elle n’a pas détectée d’authentification valide).
Paramétrage de l’authentification
Les composants graphiques étant prêts, nous allons passer à la mise en place de la mécanique d’authentification qui va les mettre en œuvre. Dans un premier temps, il faut rajouter un "Fichier de paramètres d’application" dans le projet Client. Pour les applications Blazor, comme ce fichier doit être lu lors de l’exécution, et donc depuis le navigateur, il doit être ajouté dans wwwroot
.
La section ci-dessous est ensuite rajoutée dans le fichier :
"OIDC": {
"Authority": "http://localhost:8080/realms/Coucou/",
"ClientId": "TestOIDCBlazorWASM",
"ResponseType": "code"
}
Attention à ne pas se tromper sur le protocole ni le port d’exposition de Keycloak, et si le conteneur a été arrêté depuis que vous l’avez déployé lors de l’article précédent, il faudra penser à faire un docker start iam
. De même, si vous avez nommé le realm autrement que Coucou
et le client d’authentification autrement que TestOIDCBlazorWASM
, il faut bien sûr ajuster. Noter que le fait que ce dernier soit exactement le nom de l’application est une pure convention pour s’y retrouver, mais rien ne le force. Il est simplement assez logique de les aligner, car le client au sens OIDC est une application appelée à consommer les services d’authentification, et ce sera dans notre cas l’application que nous sommes en train d’initialiser.
Dans le fichier index.html
, la ligne ci-dessous est à rajouter juste avant l’inclusion du script blazor.webassembly.js
, en toute fin du body (c’est une bonne pratique de toujours inclure les scripts en fin de page) :
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
Cette ligne de code permet d’ajouter au client toutes les fonctionnalités Javascript nécessaires pour que l’authentification se déroule correctement (en Blazor WebAssembly, tout n’est pas fait en .NET, et il reste encore des fonctionnalités réalisées en Javascript).
Dans le fichier _Imports.razor
, il faut rajouter le using ci-dessous pour que tout compile bien :
@using Microsoft.AspNetCore.Components.Authorization
Le fichier App.razor va nécessiter un peu plus de travail. Déjà, l’ensemble du code existant doit être entouré dans des balises <CascadingAuthenticationState>
, ce qui permettra de pousser automatiquement les données d’authentification dans tous les composants à l’intérieur de l’application. Ensuite, la ligne <RouteView…>
sera remplacée par le code suivant, qui nous permettra de gérer la redirection automatique (ou bien l’affichage d’un message de blocage) lorsqu’une ressource n’est désormais pas accessible :
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
C’est ce code qui appellera le RedirectToLogin
créé plus haut. Le code d’App.razor
doit être comme suit :
Il est également nécessaire pour pouvoir se connecter d’incorporer l’appel au composant LoginDisplay
créé plus haut dans la barre de menu. On intervient donc dans MainLayout.razor
pour ajouter la ligne sélectionnée dans la capture ci-dessous :
Le tout dernier ajout de code dans le client se trouve dans Program.cs
et il s’agit du code qui récupère les settings du fichier de paramétrage pour déclencher la prise en compte de l’authentification OIDC par les composants .NET :
builder.Services.AddOidcAuthentication(options => {
builder.Configuration.Bind("OIDC", options.ProviderOptions);
});
Le code doit être placé avant l’instruction .Build()
existante dans le code généré par le template Microsoft. Si vous avez modifié le nom de la section dans le fichier de settings créé plus haut, il faut bien sûr ajuster ci-dessus. Le montage réalisé revient exactement au même que si on avait indiqué les valeurs directement dans le code comme suit (mais il est bien sûr plus propre de passer par des paramètres) :
builder.Services.AddOidcAuthentication(options => {
options.ProviderOptions.Authority = "http://localhost:8080/realms/Coucou/";
options.ProviderOptions.ClientId = "TestOIDCBlazorWASM";
options.ProviderOptions.ResponseType = "code";
});
Attention, à titre de remarque, à ne jamais rien mettre de secret dans ce fichier, car il est envoyé dans le navigateur.
Test de la configuration et ajustement
Un premier lancement de l’application et un clic sur le bouton Log in montre qu’il y a une erreur :
L’IAM est bien appelée puisque l’écran qu’on voit est fourni par Keycloak. Mais le message nous précise que l’URI de redirection n’est pas correcte. Le problème vient du fait que, lors de l’article précédent, quand nous avons configuré le client d’authentification, nous sommes partis du principe que l’application serait déployée sur http://localhost:88, alors qu’elle est rendue accessible par Visual Studio en débogage sur un autre port (et en HTTPS, en plus) :
Il est donc nécessaire de retourner dans l’IAM et de changer la configuration comme suit (si vous ne voyez pas comment faire, reportez vous à l'épisode 1 de cette série d'articles) :
Une fois cette modification réalisée (inutile de relancer l’IAM, la modification est prise en compte immédiatement), on peut bien s’identifier dans le navigateur en cliquant à nouveau sur le bouton Log in
de l’application :
Connectez-vous bien sûr avec l’utilisateur que vous avez créé dans l’IAM (nommé jpg
dans le précédent article), mais pas l’administrateur déclaré au début pour Keycloak : c’est plus propre pour la sécurité. De plus, pour l’instant, le client n’a pas besoin de rôles ou de droits particuliers pour fonctionner, car nous n’avons branché que l’authentification, mais aucun mécanisme d’autorisation bloquant les appels à telle ou telle fonctionnalité en fonction de l’identité du compte, ou même du fait qu’il n’y ait pas de connexion effective (mode anonyme). En fait, le seul effet de la connexion est pour l’instant que la barre en haut à droite affiche le nom complet de la personne :
Si vous ne voyez pas de nom s’afficher, mais que le message dit tout de même "Hello", c’est certainement que vous n’aviez pas fourni ces informations lors de la création du compte :
Un dernier test pour vérifier que la déconnexion fonctionne bien et nous sommes arrivés au bout de cet épisode :
Au menu des prochains épisodes, activation de l’autorisation sur le client, ce qui nous amènera à proposer de nouveaux composants métier, à déclarer des API sur le serveur, et ensuite à authentifier et autoriser ces accès serveurs également.