Mise en place d'un service d'authentification avec le framework IdentityServer4

Hinault Donfack

Ce tutoriel vous permettra de découvrir le framework ASP.NET Core IdentityServer4 et comment l’utiliser pour mettre en place un service de gestion sécurisée de jetons, qui sera utilisé pour l’authentification de vos applications.

Commentez

- Titre : Mise en place d'un service d'authentification avec le framework IdentityServer4

- Auteur : Hinault Donfack

- Parution : 8 juin 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Hinault Donfack. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Prérequis

- III. Création du projet et configuration de IdentityServer

- III-A. Création du projet

- III-B. Configuration d’IdentityServer

- III-C. Configuration des clients et les ressources

- III-D. Modification de l’hôte

- IV. Protection d’une API en utilisant IdentityServer

- IV-A. Création de l’API

- IV-B. Configuration d’IdentityServer

- IV-C. Référencement de l’API dans IdentityServer

- V. Création et configuration du client

- V-A. Création du client

- V-B. Configuration du client

- V-C. Mise à jour de IdentityServer pour reconnaitre le client

- VI. Authentification d’un utilisateur avec OpenID Connect

- VI-A. Ajout de l’interface d’authentification

- VI-B. Transformation du projet en solution MVC

- VI-C. Ajout du support pour OpenID Connect

- VI-D. Ajout des utilisateurs pour les tests

- VII. Création et configuration d’un client utilisant OpenID Connect

- VII-A. Configuration de l’authentification OpenID Connect

- VII-B. Ajout d’une page sécurisée

- VII-C. Enregistrement du client MVC dans IdentityServer

- VII-D. Déconnexion

- VIII. Autoriser l’application MVC à accéder à l’API

- VIII-A. Modification de la configuration du client

- VIII-B. Installation du package IdentityModel

- VIII-C. Modification de la configuration de l’authentification

- VIII-D. Appel de l’API

- IX. Conclusion

- X. Remerciements

- XI. Références

I - Introduction

De nos jours, il est assez fréquent qu’une entreprise se retrouve avec plusieurs applications, services et ressources déployés à travers divers canaux (Cloud, mobile, serveurs sur site, etc.). Ces solutions sont architecturées de telle sorte que la logique d’affaires soit offerte à travers des services/microservices.

Intro

S’il s’agit d’applications, ressources ou services nécessitant au préalable une authentification pour l’accès, chaque couche devra implémenter son propre mécanisme d’authentification et de gestion des autorisations. Dans de nombreux cas, en utilisant une seule base de données pour la gestion des utilisateurs et des accès.

Au lieu de dupliquer la logique d’authentification dans chaque application ou service, il est plus judicieux de se tourner vers un service de gestion sécurisée de jetons (STS – Secure Token Service). Ce dernier jouera le rôle de service d’authentification unique pour l’ensemble de vos ressources.

Concrètement, lorsqu’un utilisateur voudra accéder à partir de son navigateur, par exemple, à l’application Web, cette dernière ne procèdera pas directement à l’authentification de celui-ci. L’application Web procèdera plutôt à une redirection de ce dernier vers le service de gestion sécurisée de jetons. L’utilisateur s’authentifiera auprès du STS et obtiendra un jeton de sécurité. Ensuite, il sera redirigé vers l’application Web à laquelle il voulait accéder initialement. Si à partir de cette application, il accède, par exemple, à une Web API, ce même jeton pourra être utilisé pour confirmer son identité et valider ses droits d’accès à cette ressource.

Par ailleurs, via la fédération, il pourra accéder à une autre application Web de l’entreprise sans avoir besoin de s’authentifier à nouveau.

Sur le marché, il existe de nombreuses solutions payantes et open source permettant de mettre en place un STS. L’une des solutions open source les plus célèbres dans l’écosystème .NET est IdentityServer.

IdentityServer est une solution open source .NET de gestion d’identité et de contrôle d’accès. Il repose sur les protocoles OpenID Connect et OAuth 2.0.

IdentityServer peut être utilisé par les entreprises pour mettre en place une solution pour :

- la protection de leurs ressources ;

- l’authentification des utilisateurs via une base de données ou des fournisseurs externes d’identité (Microsoft, Google, Facebook, etc.) ;

- la gestion des sessions et la fédération (single sign-on) ;

- la génération des jetons pour les clients ;

- la validation des jetons et bien plus.

II - Prérequis

Pour la bonne compréhension de ce tutoriel, vous devez disposer des connaissances de base en c# et en développement Web avec ASP.NET Core.

Vous devez disposer des outils suivants :

- Visual Studio 2019 ou Visual Studio Code ;

- .Net Core 3.1.

III - Création du projet et configuration de IdentityServer

III-A - Création du projet

Nous allons commencer à partir de zéro en créant une nouvelle application ASP.NET Core qui sera notre IdentityServer. Elle doit être basée sur le modèle « Vide » et n’avoir aucune authentification :

Une fois le nouveau projet créé, vous devez ajouter une référence au package IdentityServer4, en utilisant le gestionnaire de packages NuGet :

Si vous utilisez Visual Studio Code, vous pouvez utiliser la commande : Dotnet add package IdentityServer4.

III-B - Configuration d’IdentityServer

Vous aurez besoin d’enregistrer IdentityServer dans le conteneur d’injection de dépendances de ASP.NET Core et ajouter le middleware de ce dernier dans le pipeline HTTP du Framework.

Pour enregistrer IdentityServer, vous devez éditer le fichier Startup.cs et modifier la méthode ConfiguresServices comme suit :

public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential();
}

AddIdentityServer est une méthode d’extension qui permet d’enregistrer IdentityServer dans le conteneur d’IoC.

La dépendance minimale dont nous avons besoin pour l’instant est AddDeveloperSigningCredential(). Cette extension permet de créer une clé temporaire et le nécessaire pour signer les jetons (Tokens). C’est pratique pour démarrer en environnement de développement. Mais, vous ne devez pas le laisser traîner là en production et vous devez fournir le nécessaire pour gérer cela.

Pour ajouter Ie middleware IdentityServer dans le pipeline HTTP de ASP.NET Core, vous devez modifier la méthode Configure() du fichier Startup.cs et ajouter la ligne de code suivante :

app.UseIdentityServer();

Le code complet de cette méthode est  :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseIdentityServer();

app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}

III-C - Configuration des clients et les ressources

Tout client qui fait appel à notre serveur de gestion d’identité doit être un client de confiance. C’est pourquoi ce dernier doit être référencé dans l’application IdentityServer.

Par ailleurs, toute ressource (API par exemple) dont l’accès est sécurisé doit être répertoriée dans l’application IdentityServer.

Pour cela, nous allons créer une classe Config, qui aura une méthode GetClients. Elle permettra de retourner la liste des clients supportés par l’application. Une méthode GetApiResources, qui retournera la liste des API dont nous voulons sécuriser l’accès doit également être ajoutée à cette classe. Pour l’instant, puisque nous n’avons pas encore développé nos clients et nos ressources, ces listes seront vides.

Vous devez donc ajouter un nouveau fichier Config.cs à votre application avec le code suivant :

using IdentityServer4.Models;
using System.Collections.Generic;

namespace AspNetCoreIdentityServer
{
public class Config
{

public static IEnumerable<Client> GetClients()
{
return new List<Client>
{

};
}

public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{

};
}
}
}

Une fois cela fait, vous devez éditer le fichier Startup.cs et modifier la méthode ConfigureServices pour configurer IdentityServer pour qu’il utilise la liste des clients et les ressources que nous avons définies :

public void ConfigureServices(IServiceCollection services)
{

//configure identity server with in-memory stores, keys, clients and resources
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

}

C’est tout. Nous venons de faire le minimum pour intégrer IdentityServer à notre projet. Nous pouvons désormais l’utiliser comme service de gestion sécurisée des accès pour nos applications.

Mais avant, nous allons faire quelques modifications pour nous assurer que les clients pointeront toujours sur la bonne application.

III-D - Modification de l’hôte

Nous devons nous assurer que notre application IdentityServer sera toujours accessible via la même adresse lorsqu’elle est en exécution. Par ailleurs, en environnement de développement et lorsqu’on est en mode apprentissage, il est intéressant de voir en temps réel les logs de notre application dans la console.

Pour cela, nous allons accéder à l’onglet « Déboguer » dans les propriétés de notre projet. Nous allons dérouler la zone « Profil » et sélectionner le nom de l'application (AspNetCoreIdentityServer).

Les URL suivantes doivent être définies dans le champ URL de l’application, si ce n’est pas le cas : https://localhost:5001;http://localhost:5000

Générez et exécutez votre application.

Les projets ASP.NET Core sont configurés pour utiliser par défaut SSL. Pour éviter les avertissements SSL dans le navigateur, vous devez accepter le certificat autosigné généré par ASP.NET Core. Une notification s’affichera à cet effet à la première exécution de votre application.

Ouvrez votre navigateur et saisissez l’adresse URL :

http://localhost:5000/.well-known/openid-configuration

ou

https://localhost:5001/.well-known/openid-configuration

Vous obtiendrez le résultat suivant :

Vous venez de mettre en place votre service de gestion sécurisée de jetons. Il est prêt pour la sécurisation de vos ressources et l’authentification de vos clients.

IV - Protection d’une API en utilisant IdentityServer

Dans cette partie, nous verrons comment sécuriser une API en utilisant IdentityServer. Pour qu’un client puisse consommer l’API, il devra au préalable s’authentifier auprès de IdentityServer pour obtenir un jeton d’accès qu’il utilisera pour accéder à l’API.

IV-A - Création de l’API

La première chose à faire sera la création de l’API. Nous allons ajouter à notre solution une nouvelle application ASP.NET Core Web API :

Une fois l’application créée, nous allons ajouter un nouveau Contrôleur d’API ayant pour nom SecureController, avec le code suivant :

[Route("api/[controller]")]
[Authorize]
public class SecureController : Controller
{
// GET: api/<controller>
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}

Vous remarquerez que notre contrôleur est décoré avec l’attribut [Authorize]. Ce qui veut dire que tout accès aux méthodes de cette classe est conditionné par l’obtention au préalable des droits. Pour un client ayant des accès, il pourra visualiser les informations de revendication (Claims) associées à son profile utilisateur.

IV-B - Configuration d’IdentityServer

Nous allons configurer IdentityServer afin que ce dernier puisse procéder à la validation du jeton de sécurité d’un client, afin de s’assurer que ce dernier provient d’un client de confiance ayant les autorisations nécessaires pour accéder à l’API.

Pour cela, nous allons dans un premier temps ajouter le package Microsoft.AspNetCore.Authentication.JwtBearer.

Si vous utilisez Visual Studio Code, vous devez exécuter dans le terminal intégré la commande : dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Ensuite, nous allons éditer le fichier Startup.cs et modifier la méthode ConfigureServices :

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;

options.Audience = "testapi";
});
}

La méthode AddAuthentication() va permettre d’enregistrer auprès du conteneur d’IoC le service d’authentification en utilisant « Bearer » comme schéma par défaut. Cette configuration permet à notre application d’être basée sur une authentification par jeton (token authentication) basique. Avant d’accéder à notre API, le client sera donc obligé de passer un jeton d’autorisation dans l’entête HTTP.

La méthode AddJwtBearer va permettre d’enregistrer le service qui sera utilisé pour valider le jeton fourni par le client.

Vous devez aussi modifier la méthode Configure de la classe Startup pour ajouter au pipeline http d’ASP.NET Core le middleware d’authentification :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

Pour finir, je vous recommande également de modifier les propriétés du projet pour que l’API soit toujours accessible via les adresses : https://localhost:5003 et http://localhost:5002.

IV-C - Référencement de l’API dans IdentityServer

Pour que notre API soit protégée, nous devons la référencer auprès de IdentityServer comme ressource.

Pour cela, nous allons simplement éditer le fichier Config.cs dans l’application IdentityServer et ajouter notre API dans la liste des ApiResources :

public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("testapi", "My Test API")
};
}

Le premier paramètre est le nom de l’API. Il doit être identique au nom d’API défini dans les options lors de l’appel à AddJwtBearer dans le projet d’API.

Si vous essayez d’accéder à l’adresse https://localhost:5003/api/secure dans votre navigateur, vous aurez l’erreur 401. Cela signifie qu’une authentification est nécessaire pour accéder à cette ressource.

Pour déboguer et exécuter vos deux applications simultanément. Vous pouvez ouvrir deux invites de commande, chacune positionnée sur le répertoire de chaque projet et exécuter la commande dotnet run.

V - Création et configuration du client

Dans cette partie, nous allons créer le client qui sera une application Console .NET Core. Nous allons écrire le code nécessaire pour permettre à ce denier de demander un jeton de sécurité à l’application Identitty Server, ensuite utiliser ce dernier pour s’authentifier auprès de l’API et accéder aux fonctionnalités de cette dernière.

V-A - Création du client

Pour commencer, nous allons créer une nouvelle application console .NET Core :

Nous allons éditer le fichier Program.cs et ajouter le code nécessaire pour appeler notre API. Le code complet de la classe Program est le suivant :

class Program
{
static void Main(string[] args) => CallWebApi().GetAwaiter().GetResult();

static async Task CallWebApi()

{
var apiclient = new HttpClient();

var response = await apiclient.GetAsync("https://localhost:5003/api/secure");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}
}

Enregistrez et exécutez votre application.

Vous aurez le message suivant à l’écran :

L’accès à la ressource pour notre application a été refusé. Nous allons maintenant configurer cette dernière pour qu’elle demande un jeton d’authentification à IdentityServer et qu’elle utilise ce dernier pour accéder à la ressource voulue.

V-B - Configuration du client

Pour accéder facilement à IdentityServer dans notre client, nous allons utiliser la bibliothèque IdentityModel. La première chose à faire sera donc l’ajout du package correspondant à notre application en utilisant NuGet :

Nous allons utiliser la méthode GetAsync() de la classe DiscoveryClient pour récupérer les métadonnées exposées par le EndPoint de l’application IdentityServer. Cette méthode prend en paramètre l’URL de notre application IdentityServer. Nous devons nous assurer que le EndPoint est accessible avant de continuer :

var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}

Ensuite, nous devons initialiser un nouvel objet TokenClient, en lui passant en paramètre le TokenEndpoint, l’ID du client et le secret.

Nous allons utiliser la méthode RequestClientCredentialsAsync pour demander un jeton d’authentification pour accéder à l’API. Cette méthode prend en paramètre le nom de l’API auquel on veut accéder, tel qu’il est répertorié auprès de IdentityServer. Nous devons nous assurer que le jeton a été obtenu avant de continuer :

// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,

ClientId = "consoleappclient",
ClientSecret = "secret",
Scope = "testapi"
});

if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}

Console.WriteLine(tokenResponse.Json);

Nous allons pour finir utiliser la méthode SetBearerToken de HttpClient() pour inscrire le jeton dans l’entête HTTP de notre requête :

var apiclient = new HttpClient();
apiclient.SetBearerToken(tokenResponse.AccessToken);

Le code complet de la méthode CallWebApi devient ceci :

static async Task CallWebApi()

{
var client = new HttpClient();
// discover endpoints from metadata
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}

// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,

ClientId = "consoleappclient",
ClientSecret = "secret",
Scope = "testapi"
});

if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}

Console.WriteLine(tokenResponse.Json);

// call api
var apiclient = new HttpClient();
apiclient.SetBearerToken(tokenResponse.AccessToken);

var response = await apiclient.GetAsync("https://localhost:5003/secure");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}

V-C - Mise à jour de IdentityServer pour reconnaitre le client

Toute la configuration nécessaire pour accéder à l’API a été effectuée côté client. Toutefois, si ce dernier essaye d’accéder à la ressource, il n’aura toujours pas le droit. Cela est dû au fait qu’il n’est pas encore connu par IdentityServer. Nous devons donc enregistrer ce dernier et définir à quoi il a accès.

Pour cela, nous devons éditer le fichier Config.cs et ajouter un nouveau client à la liste des clients. Nous devons lui donner le même nom et le même secret que nous avons défini dans l'application console.

Le code de la méthode GetClients de la classe Config devrait donc ressembler à ceci :

public static IEnumerable<Client> GetClients()
{
return new List<Client>
{

new Client
{
ClientId = "ConsoleAppClient",
AllowedGrantTypes = GrantTypes.ClientCredentials,

ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "testapi" }
}

};
}

Vous remarquerez que nous avons également défini la ressource à laquelle le client doit accéder.

Enregistrez les modifications. Exécutez l’application IdentityServer, l’API et enfin l’application console.

Vous aurez le résultat suivant :

Le jeton qui est généré par IdentityServer et utilisé par le client pour accéder à la ressource est au format JWT(JSON Web Token). Il s’agit d’un jeton sécurisé qui contient toutes les informations nécessaires pour confirmer l’identité du client et lui donner accès à la ressource demandée. Si vous essayez de décoder le jeton avec https://jwt.io, vous obtiendrez ce qui suit :

Nous venons d’accéder à notre ressource sécurisée en utilisant un jeton de sécurité provenant d’IdentityServer.

VI - Authentification d’un utilisateur avec OpenID Connect

Nous disposons d’une application Web dont nous voulons sécuriser certaines pages. Pour accéder à ces pages, l’utilisateur doit s’authentifier au préalable en utilisant son compte.

Nous devons donc intégrer cette nouvelle application à notre application IdentityServer. Lorsque l’utilisateur vaudra accéder à une page protégée, il sera redirigé vers IdentityServer qui affichera une fenêtre d’authentification. Une fois ce dernier authentifié, il sera redirigé vers la page à laquelle il voulait accéder.

VI-A - Ajout de l’interface d’authentification

Actuellement notre solution IdentityServer ne dispose d’aucune interface. Tout ce qu’elle est en mesure d’afficher dans un navigateur c’est un « Hello World! ».

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage()
;

}

app.UseIdentityServer();

app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}

Elle ne dispose donc d’aucune vue, contrôleur, etc. Pourtant, nous avons besoin d’une solution permettant d’authentifier un utilisateur via un formulaire, mettre fin à sa session, etc. Pour mettre cela en place, nous allons nous appuyer sur un modèle Quickstart existant fourni par les développeurs de IdentityServer4.

Ce modèle est disponible dans le repository GitHub suivant https://github.com/IdentityServer/IdentityServer4.Quickstart.UI/tree/release/Views. Vous pouvez le télécharger et copier/coller les dossiers Quickstart, Views et wwwroot dans votre projet.

Vous pouvez aussi simplement ouvrir le terminal Powershell dans le dossier racine du projet et exécuter la commande :

iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/master/getmaster.ps1'))

Je n’entrerai pas dans les détails d’implémentation de ce modèle.

VI-B - Transformation du projet en solution MVC

Le Quickstart que nous avons intégré repose sur ASP.NET MVC. Nous allons apporter quelques modifications à notre projet pour prendre en charge ASP.NET Core MVC.

La première chose à faire sera de modifier la méthode ConfigureServices() du projet IdentityServer et ajouter la ligne de code suivante au début :

services.AddControllersWithViews();

Cette ligne de code permet d’enregistrer les services pour la prise en charge de ASP.NET Core MVC.

Par la suite, vous devez modifier la méthode Configure pour intégrer les middlewares nécessaires à la gestion des fichiers statistiques et le routage. Par ailleurs, vous devez supprimer la ligne de code permettant d’afficher le « Hello Word! » :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();
app.UseRouting();

app.UseIdentityServer();

app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}

VI-C - Ajout du support pour OpenID Connect

OpenID Connect est une couche d'identification basée sur le protocole OAuth 2.0, qui autorise les clients à vérifier l'identité d'un utilisateur final en se basant sur l'authentification fournie par un serveur d'autorisation, dont IdentityServer. L’authentification d’un utilisateur via un formulaire avec IdentityServer repose sur OpenID Connect.

L’implémentation repose sur le concept de scopes (portées). Nous allons définir comme nous l’avons fait pour l’API, les ressources auxquelles le client doit accéder. Sauf qu’ici, il s’agit des informations du profil utilisateur (id, nom, email, etc.) que nous souhaitons partager avec le client.

Pour le faire, nous allons éditer le fichier Config.cs et ajouter les lignes de code suivantes :

public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}

Une fois cela fait, nous devons modifier la méthode ConfigureServices du fichier Startup.cs pour ajouter cette nouvelle ressource à la configuration d’IdentityServer. Cela se fait en utilisant la méthode d’extension AddInMemoryIdentityResources lors de l’appel de AddIdentityServer() :

public void ConfigureServices(IServiceCollection services)
{

services.AddControllersWithViews();
//configure identity server with in-memory stores, keys, clients and resources
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

}

VI-D - Ajout des utilisateurs pour les tests

Les informations saisies par l’utilisateur dans le formulaire d’authentification doivent être validées avant de lui donner les accès. Cependant nous ne disposons pas d’une base de données utilisateurs que nous pouvons utiliser. IdentityServer pour des besoins de tests permet de définir et utiliser une liste de type TestUser.

Nous allons une nouvelle fois modifier le fichier Config.cs et ajouter le code suivant pour définir notre liste d’utilisateurs :

public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser{SubjectId = "818727", Username = "alice", Password = "alice",
Claims =
{
new Claim(JwtClaimTypes.Name, "Alice Smith"),
new Claim(JwtClaimTypes.GivenName, "Alice"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
}
},
new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob",
Claims =
{
new Claim(JwtClaimTypes.Name, "Bob Smith"),
new Claim(JwtClaimTypes.GivenName, "Bob"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json),
new Claim("location", "somewhere")
}
}
};
}

Nous devons également modifier la méthode ConfigureServices du fichier Startup.cs pour ajouter cette liste à la configuration d’IdentityServer. Cela se fait en utilisant la méthode d’extension AddTestUsers lors de l’appel de AddIdentityServer() :

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

//configure identity server with in-memory stores, keys, clients and resources
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers()); ;

}

Enregistrez les modifications et exécutez l’application. Vous aurez le résultat suivant affiché dans le navigateur.

Si vous cliquez sur « Click here to manage your stored grants. », vous essayerez d’accéder à une page protégée. De ce fait, vous serez redirigé vers la page d’authentification :

VII - Création et configuration d’un client utilisant OpenID Connect

Nous allons maintenant créer une nouvelle application ASP.NET Core MVC. L’utilisateur pour accéder à une page sécurisée de cette application, devra au préalable s’authentifier. Et l’authentification sera gérée par notre solution d’authentification centralisée.

Vous allez donc ajouter une nouvelle application ASP.NET Core MVC à votre solution :

Vous devez modifier l’application pour utiliser les ports 5004 et 5005 respectivement en HTTP et en HTTPS :

VII-A - Configuration de l’authentification OpenID Connect

Nous devons configurer notre application pour utiliser l’authentification OpenID Connect, car c’est ce qui est supporté par notre serveur d’authentification IdentityServer.

Pour commencer, vous allez ajouter le package Microsoft.AspNetCore.Authentication.OpenIdConnect à votre projet.

Nous allons ensuite éditer le fichier Startup.cs et ajouter les lignes de code suivantes dans la méthode ConfigureServices :

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";

options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;

options.ClientId = "mvcappclient";
options.SaveTokens = true;
});

La méthode AddAuthentication() va permettre d’enregistrer auprès du conteneur d’IoC le service d’authentification en utilisant « Cookies » par défaut. Nous avons besoin de définir DefaultChallengeScheme à « oidc » parce que nous utilisons OpenID Connect.

L’extension AddCookie va permettra d’ajouter le handler pour la gestion des cookies.

L’extension AddOpenIdConnect, quant à elle, va permettre de configurer le handler pour utiliser le protocole OpenID Connect. Nous devons spécifier l’URL de l’application IdentityServer (Authority) et l’ID du client (ClientID). Nous devons renseigner le même ID Client dans IdentityServer. Par ailleurs, SaveTokens est utilisé pour assurer la persistance du jeton retourné par IdentityServer dans le cookies. SignInScheme est utilisé par le handler de gestion de cookies pour générer un cookie lorsque le processus OpenID Connect est complété.

Le code complet de la méthode ConfigureServices est le suivant :

public void ConfigureServices(IServiceCollection services)
{

services.AddControllersWithViews();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";

options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;

options.ClientId = "mvcappclient";
options.SaveTokens = true;
});

}

Pour nous assurer que les services de gestion d’authentification soient exécutés à chaque requête, nous allons ajouter le middleware pour l’authentification dans le pipeline HTTP de ASP.NET Core.

Cela se fait en ajoutant UseAuthentication dans la méthode Configure du fichier Startup :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseAuthentication();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

VII-B - Ajout d’une page sécurisée

Maintenant, nous allons ajouter une page dont l’accès est sécurisé. Dans le HomeController, ajoutez une nouvelle méthode d’action Secure, avec le code suivant :

[Authorize]
public IActionResult Secure()
{
ViewData["Message"] = "Secure page.";

return View();
}

Elle doit être décorée avec l’attribut [Autorize]. C’est cet attribut qui permet au système d’authentification de ASP.NET de savoir que l’accès à cette ressource est sécurisé et que l’utilisateur doit obtenir au préalable les droits nécessaires.

Faites un clic droit sur cette méthode, puis sélectionnez ajouter une vue. Remplacez le code de cette vue par ce qui suit :

@{
ViewData["Title"] = "Secure";
}
<h2>@ViewData["Title"]</h2>
<h3>User claims</h3>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>

}
</dl>

Enregistrez et exécutez votre application, ainsi que l’application IdentityServer en utilisant l’invite de commande et la commande Dotnet Run. Si vous entrez l’URL suivante https://localhost:5005/Home/Secure dans votre navigateur, vous serez redirigé vers la page suivante :

IdentityServer ne reconnait pas notre client. Celui-ci doit faire partie de sa liste des clients.

VII-C - Enregistrement du client MVC dans IdentityServer

Vous allez éditer le fichier Config.cs et ajouter notre application dans la liste des clients :

public static IEnumerable<Client> GetClients()
{
return new List<Client>
{

new Client
{
ClientId = "consoleappclient",
AllowedGrantTypes = GrantTypes.ClientCredentials,

ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "testapi" }
},

// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvcappclient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,

RedirectUris = { "https://localhost:5005/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" },

AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}

};
}

L’ID doit être le même que celui spécifié lors de la configuration de l’authentification côté client. Le « Grant Type » doit être à Implicit. Il s’agit du mode le plus optimisé pour les applications Web. Ce mode permet le transfert de tous les jetons via le navigateur.

AllowedScopes permet de définir les informations qui doivent être partagées avec le client. Nous voulons que les données de profil de l’utilisateur puissent être partagées et que la transaction se fasse via le protocole OpenID.

Une fois cela fait, exécutez de nouveau vos applications et accédez à la page sécurisée (https://localhost:5005/Home/Secure). Vous serez maintenant redirigé vers la page de connexion :

Une fois connectée, une page de consentement s’affichera afin d’obtenir l’approbation de l’utilisateur avant de partager ses données de profil avec l’application MVC :

Par défaut, le consentement est à « True ». Si vous ne voulez pas que le consentement de l’utilisateur soit demandé au préalable, vous devez mettre la propriété RequireConsent à False lors de l’enregistrement du client :

new Client
{
ClientId = "mvcappclient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
RequireConsent = false,
RedirectUris = { "https://localhost:5005/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" },

AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}

Lorsque vous cliquez sur « Yes, Allow », vous êtes redirigé vers la page à laquelle vous vouliez accéder :

VII-D - Déconnexion

La déconnexion avec IdentityServer est aussi simple que supprimer les cookies d’authentification. Pour mettre en place la déconnexion, vous devez simplement ajouter la méthode d’action et les lignes de code suivantes côté client :

public async Task Logout()
{
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc");
}

Si vous exécutez votre application et essayez de vous déconnecter, vous allez vous rendre compte que vous allez être automatiquement redirigé vers la page de confirmation de déconnexion de l’application IdentityServer (cette page est déjà mise en place dans le modèle Quickstart que nous avons intégré dans notre application).

En cliquant sur le lien affiché, vous êtes redirigé vers la page d’accueil du client MVC. Vous devez avoir au préalable défini l’URL de retour. Nous l’avons fait lors de l’enregistrement du client.

// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvcappclient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
RequireConsent = false,
RedirectUris = { "https://localhost:5005/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" },

AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}

Pour que la redirection soit automatique, vous devez simplement éditer le fichier AccountOptions.cs qui se trouve dans le dossier Quickstart/Account et mettre à true le champ

AutomaticRedirectAfterSignOut :

public class AccountOptions
{
public static bool AllowLocalLogin = true;
public static bool AllowRememberLogin = true;
public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);

public static bool ShowLogoutPrompt = true;
public static bool AutomaticRedirectAfterSignOut = true;

// specify the Windows authentication scheme being used
public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme;
// if user uses windows auth, should we load the groups from windows
public static bool IncludeWindowsGroups = false;

public static string InvalidCredentialsErrorMessage = "Invalid username or password";
}

VIII - Autoriser l’application MVC à accéder à l’API

Nous avons développé un client MVC capable d’utiliser IdentityServer et OpenID Connect pour s’authentifier via un formulaire et obtenir l’accès aux ressources sécurisées. Nous voulons maintenant qu’une fois l’utilisateur authentifié, ce dernier puisse obtenir un jeton d’accès pour appeler l’API que nous avons développée précédemment.

IdentityServer implémente le protocole OAuth 2.0 pour la gestion des jetons. Nous aurons donc recours à cette étape à un mode hybride qui utilise à la fois OpenID Connect et OAUth 2.0. Voyons comment procéder.

VIII-A - Modification de la configuration du client

Dans l’application IdentityServer, nous allons modifier la configuration de l’application MVC dans le fichier Config.cs. La nouvelle configuration est la suivante :

// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvcappclient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent = false,
RedirectUris = { "https://localhost:5005/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" },

AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"testapi"
},

AllowOfflineAccess = true
}

Vous allez remarquer que nous avons changé le GrantTypes de « Implicit » à « HybridAndClientCredentials ». Le mode implicite est pratique pour la transmission des jetons d’identification via le navigateur. Par contre, en ce qui concerne les jetons d’accès (dont nous aurons besoin pour accéder à l’API), qui sont plus sensibles que les jetons d’identification ce mode est moins pratique. Pour pallier cela, un mode code est offert.

Le mode code permet au client d’obtenir dans un premier temps un jeton d’identification qui est transmis par le navigateur. Une fois l’authenticité du jeton validée, le client ouvre un canal de retour avec IdentityServer pour obtenir un jeton d’accès.

Nous avons ensuite donné un mot de passe (ClientSecrets) qui sera utilisé pour fournir le jeton d’accès via le canal de retour.

Enfin, nous avons ajouté l’API dans les scopes afin que le client puisse accéder à ce dernier et mettre « AllowOfflineAccess » à « True ». Cela permet de demander l’actualisation des jetons pour des accès de longue durée aux API.

C’est tout ce qui est à modifier coté IdentityServer.

VIII-B - Installation du package IdentityModel

La première chose à faire côté client est l’installation du package IdentityModel dans l’application MVC. Il s’agit de la bibliothèque utilisée côté client pour accéder facilement au « discovery endpoint » d’IdentityServer.

VIII-C - Modification de la configuration de l’authentification

Nous devons modifier la méthode ConfigureServices du fichier Startup.cs et remplacer le code de la configuration de l’authentification par ce qui suit :

services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";

options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;

options.ClientId = "mvcappclient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;

options.Scope.Add("testapi");
options.Scope.Add("offline_access");
});

Nous avons ajouté le ClientSecret afin qu’il puisse correspondre à la valeur définie dans IdentityServer. Nous avons également ajouté l’ID de l’API (testapi) et « offline_access » dans les scopes pour avoir les mêmes niveaux d’accès des deux côtés. La valeur « code id_token » pour le champ ResponseType permet de spécifier que nous utilisons le mode code.

VIII-D - Appel de l’API

Pour appeler l’API, le client devra d’abord s’authentifier par formulaire en utilisant OpenID Connect. Il recevra ensuite un jeton d’accès qu’il pourra utiliser pour accéder à l’API. Le code correspondant est le suivant :

public async Task<IActionResult> CallApiUsingUserAccessToken()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");

var client = new HttpClient();
client.SetBearerToken(accessToken);
var content = await client.GetStringAsync("https://localhost:5003/api/secure");

ViewBag.Json = JArray.Parse(content).ToString();
return View();
}

Ce mode est pratique si on a besoin que l’utilisateur soit identifié avant de pouvoir accéder à l’API. Vous pouvez ajouter la vue correspondante avec le code suivant :

@{
ViewData["Title"] = "CallApiUsingUserAccessToken";
}

<h2>API response</h2>

<pre>@ViewBag.Json</pre>

Pour tester l’application, vous devez exécuter à la fois les trois applications (IdentityServer, TestAPI et le client MVC). Une fois les applications en cours d’exécution, utilisez le lien suivant pour visualiser les résultats : https://localhost:5005/home/CallApiUsingUserAccessToken

IX - Conclusion

IdentityServer représente une belle option open source pour mettre en place un service de gestion sécurisée de jetons pour offrir un mécanisme fiable d’authentification et de sécurisation de ses ressources. La plateforme s’appuie sur des standards ouverts, notamment OpenID Connect et OAuth 2.0. Elle bénéficie d’un fort soutient de la communauté open source et des grandes firmes de l’IT. Ce qui en fait un produit viable.

Ce guide a pour objectif de vous permettre de démarrer avec IdentityServer4. Vous pouvez vous référer à la documentation officielle pour des notions un peu plus avancées. Il faut dire que cette dernière est très bien faite et assez pratique.

Le code source du projet d’exemple est disponible sur Github : https://github.com/hinault/identityserver

X - Remerciements

Je tiens à remercier Claude Leloup pour sa relecture orthographique.

XI - Références

- Le code source du projet d’exemple

- La page Github d’IdentityServer

- La documentation officielle

- Des billets de blog en français sur IdentityServer