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.
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.