Blog ENI : Toute la veille numérique !
💥 Un livre PAPIER acheté
= La version EN LIGNE offerte pendant 1 an !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. OAuth 2 et OpenID Connect
  3. ASP.Net Identity x Entity framework
Extrait - OAuth 2 et OpenID Connect Sécurisez vos applications .Net avec IdentityServer
Extraits du livre
OAuth 2 et OpenID Connect Sécurisez vos applications .Net avec IdentityServer Revenir à la page d'achat du livre

ASP.Net Identity x Entity framework

Présentation

Comme évoqué précédemment, ASP.Net Identity se repose sur Entity Framework pour persister les données en base de données. Le choix de la base de données vous appartient, Entity Framework pouvant être interfacé avec de nombreuses bases de données relationnelles ou non telles que SQL Server, Postgre SQL, Maria DB, Mongo DB, Cosmos DB, etc. Dans ce livre, SQL Server sera utilisé comme base de données. Mais si vous optez pour une autre base de données, cela n’aura qu’un impact extrêmement limité sur votre code.

Le DbContext

Notre template contient déjà une injection des services nécessaires au bon fonctionnement d’Entity Framework :

var connectionString = builder.Configuration.GetConnectionString
("DefaultConnection") ?? throw new InvalidOperationException
("Connection string 'DefaultConnection' not found.");  
builder.Services.AddDbContext<ApplicationDbContext>(options =>  
    options.UseSqlServer(connectionString));  
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 

La méthode AddDbContext() est une méthode de commodité qui masque une réalité plus complexe :

public static IServiceCollection AddDbContext  
     <TContextService, 
[DynamicallyAccessedMembers(DbContext.DynamicallyAccessedMemberTypes)]  
TContextImplementation>(  
         this IServiceCollection serviceCollection,  
         Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction, 
         ServiceLifetime contextLifetime = ServiceLifetime.Scoped,  
         ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)  
     where TContextImplementation : DbContext, TContextService  
 {  
     if (contextLifetime == ServiceLifetime.Singleton)  
     {  
         optionsLifetime = ServiceLifetime.Singleton;  
     }  
  
     if (optionsAction != null)  
     {  
         CheckContextConstructors<TContextImplementation>();  
     }  
   
     AddCoreServices<TContextImplementation>(serviceCollection, 
optionsAction, optionsLifetime);  
  
     if (serviceCollection.Any(d => d.ServiceType == 
typeof(IDbContextFactorySource<TContextImplementation>)))  
     {  ...

Étendre un utilisateur

Notre classe ApplicationDbContext en héritant de la classe IdentityDbContext hérite de plusieurs tables et comportements liés à ces tables (liaisons entre tables, index, etc.) qui vont nous permettre de gérer nos utilisateurs.

Mais comment faire si nous souhaitons étendre notre classe utilisateur avec des propriétés qui n’y figurent pas comme le nom et le prénom ? Aussi étrange que cela puisse paraître, la classe IdentityUser ne contient pas le nom et le prénom de votre utilisateur. Cette classe ne contient que des propriétés "techniques" liées à l’authentification : UserName pour se connecter, Email pour les mots de passe oubliés, PhoneNumber pour la double authentification, etc., mais rien concernant la personne.

Il existe deux approches dans cette situation :

La première approche est d’étendre la classe IdentityUser en créant une nouvelle classe utilisateur qui héritera de IdentityUser. Le contexte prenant comme type utilisateur un type générique TUser dont la contrainte est d’être un IdentityUser<TKey>, cela ne pose aucun problème.

La deuxième approche consiste à mettre en place une autre classe représentant l’humain et qui sera liée à la classe IdentityUser via une clé étrangère. Cette approche présente plusieurs inconvénients comme le fait de devoir quasi systématiquement faire des jointures lors des requêtes. Les classes et les méthodes de gestion des utilisateurs, les rôles, etc., ne sont pas prévus pour fonctionner avec deux entités liées par une jointure. Il faudra donc s’en passer et tout faire à la main. Ce n’est pas insurmontable, parfois c’est souhaitable de passer sur des implémentations manuelles, mais le plus souvent rester dans le cadre du framework est l’approche la plus appropriée.

Vous l’aurez deviné, c’est l’extension de la classe utilisateur que nous allons utiliser.

Pour cela, nous allons créer dans le dossier Models une classe ApplicationUser que nous allons faire hériter de IdentityUser. Et nous allons y ajouter des propriétés de civilité, nom et prénom....

ASP.Net Identity : configuration approfondie

Dans le chapitre ASP.Net Identity, nous avons vu les bases de la configuration d’ASP.Net Identity au travers de la méthode AddDefaultIdentity<TUser>(). Cette méthode encapsule l’injection de tout un ensemble de services, dont les vues par défaut.

public static IdentityBuilder AddDefaultIdentity<TUser>(this 
IServiceCollection services, Action<IdentityOptions> 
configureOptions) where TUser : class  
{  
  services.AddAuthentication(o =>  
  {  
    o.DefaultScheme = IdentityConstants.ApplicationScheme;  
    o.DefaultSignInScheme = IdentityConstants.ExternalScheme;  
  })  
  .AddIdentityCookies(o => { });  
  
  return services.AddIdentityCore<TUser>(o =>  
  {  
    o.Stores.MaxLengthForKeys = 128;  
    configureOptions?.Invoke(o);  
  })  
  .AddDefaultUI()  
  .AddDefaultTokenProviders();  
} 

Ces vues par défaut sont conçues pour fonctionner avec deux classes : la classe UserManager<TUser> et la classe SignInManager<TUser> dans lesquelles TUser est un IdentityUser. Comme nous avons modifié notre classe utilisateur, les vues par défaut ne peuvent plus fonctionner. Pour vous en convaincre, lancez le projet et accédez à la page /Identity/Account/Login, vous obtiendrez une erreur :

InvalidOperationException: No service for type  
'Microsoft.AspNetCore.Identity.UserManager`1
[Microsoft.AspNetCore.Identity.IdentityUser]' has been registered. 

La solution la plus simple est de générer les vues comme nous l’avons fait précédemment (cf. chapitre ASP.Net Identity, section Identity UI), et dans chaque vue qui consomme le UserManager ou le SignInManager, remplacer le paramètre générique TUser pour le passer de IdentityUser à ApplicationUser. Cette approche fonctionne mais nous oblige à conserver exactement le schéma de vues par défaut. Si nous souhaitons changer des pages pour avoir un flux...