Aller au contenu principal

Utilisateurs sans stockage en base

Par défaut, chaque connexion enregistre l'utilisateur dans les tables ASP.NET Core Identity : AspNetUsers, ses rôles (AspNetUserRoles), ses claims personnalisés (AspNetUserClaims), ses logins et jetons externes (AspNetUserLogins, AspNetUserTokens). OpenIddict enregistre aussi chaque code d'autorisation, avec les claims de l'utilisateur, dans OpenIddictTokens.

Une application qui ne doit enregistrer aucune donnée personnelle peut désactiver ces stockages. L'identité de l'utilisateur (login, e-mail, rôles, claims) vit alors seulement :

  • dans le cookie d'authentification .{RootName}.Identity, chiffré par Data Protection ;
  • dans les codes et jetons OpenIddict, autoportés et chiffrés eux aussi par Data Protection, qui ne sont pas enregistrés en base.

Côté authentification, la seule écriture restante est une autorisation OpenIddict par renouvellement de jeton, qui ne contient qu'un identifiant aléatoire, le client et les scopes (voir Fonctionnement).

Activation​

Main.cs
protected override void OnConfigureAuthentification(AuthenticationOptions options)
{
// Aucune écriture dans les tables Identity ni dans OpenIddictTokens.
options.UserStorage = UserStorageMode.None;
}

La valeur par défaut, UserStorageMode.Database, garde le comportement habituel.

Prérequis​

  • SSO ou Windows uniquement. Au démarrage, une InvalidOperationException est levée si AllowFormAuthentication est vrai, ou si MultiFactorAuthenticationRequired ou MultiFactorAuthenticationRoleName sont renseignés : le formulaire et la MFA exigent un utilisateur stocké.
  • Rôles donnés à chaque connexion. Les rôles ne peuvent pas être lus en base : l'application les ajoute dans OnSignIn ou OnSignInAsync avec context.AddCustomRole(...). IsCustomRolesEnabled est forcé à true et ne peut pas être remis à false (InvalidOperationException). Sans rôle, la connexion est refusée (AuthNoAppAccess).
Main.cs
protected override async Task OnSignInAsync(AuthenticationContext context, ApplicationUser user)
{
// Nom affiché, conservé uniquement dans le cookie et les jetons.
// Un claim ne peut pas avoir une valeur nulle : certains fournisseurs ne donnent pas de nom.
string displayName = context.IdentitySource.Claims?.GetDisplayName();
if (!string.IsNullOrEmpty(displayName))
{
context.AddCustomClaim("DisplayName", displayName);
}
// Rôles calculés à chaque connexion (groupes du fournisseur d'identité, configuration...).
context.AddCustomRole(Roles.User.ToString());
if (await IsAdministratorAsync(context))
{
context.AddCustomRole(Roles.Admin.ToString());
}
}

Le claim DisplayName va déjà dans les deux jetons. Un autre claim personnalisé utilisé par une policy du serveur doit être déclaré dans AuthenticationOptions.AccessTokenClaims : voir Policy fondée sur un claim personnalisé.

Fonctionnement​

ÉtapeMode DatabaseMode None
Recherche de l'utilisateurFindByEmailAsync / FindByNameAsyncAucune. GetIdentityUserAsync et OnUnknownUserAsync ne sont pas appelés.
Utilisateur passé à OnSignIn(Async)Utilisateur de la baseUtilisateur transitoire, jamais attaché au DbContext : UserName = login, Email = login (connexion par e-mail) ou claim e-mail du fournisseur.
Rôles, claims, logins externesEnregistrés en baseNon enregistrés. Les jetons du fournisseur restent dans le cookie.
Identifiant (sub)AspNetUsers.IdGuid aléatoire à chaque connexion : rien ne relie deux sessions d'une même personne.
CookieGlissant, security stamp relu en base toutes les 30 minutesExpiration absolue (SsoAuthenticationCookieLifeTime), aucune relecture.
Codes et jetons OpenIddictPrincipal reconstruit depuis la base, codes enregistrés dans OpenIddictTokensPrincipal copié depuis le cookie, aucune ligne dans OpenIddictTokens.
Autorisation OpenIddictPermanentAdHoc (identifiant aléatoire, client, scopes), supprimée par la purge Quartz.
Jeton de rafraîchissementÉmis si le client demande offline_accessJamais émis : le scope offline_access est retiré.
Reconnexion (popup)Comparaison des identifiants en baseLe login du fournisseur doit être celui du cookie de la session.

Conséquences​

Durée de session

Rien ne relit l'utilisateur pendant la session. Un changement de rôle chez le fournisseur d'identité (retrait d'un groupe par exemple) ne s'applique qu'à la connexion suivante, au plus tard à l'expiration du cookie : SsoAuthenticationCookieLifeTime, 1 jour par défaut. Cette expiration est absolue : les renouvellements silencieux des jetons ne la prolongent pas. Réduisez SsoAuthenticationCookieLifeTime, ou SessionLifetime (en minutes), si le retrait des droits doit être plus rapide.

  • HttpContext.GetUserId() renvoie l'identifiant de la session, pas d'une personne. Les traces de modification (LgTrackedChanges.UserId) portent donc cet identifiant de session et le claim DisplayName.
  • Sans enregistrement des codes et jetons, OpenIddict ne peut ni détecter la réutilisation d'un code d'autorisation (le client Blazor de Lagoon utilise PKCE, qui la compense en grande partie), ni révoquer un jeton : un jeton d'accès reste valide jusqu'à son expiration (AccessTokenLifetime, 30 minutes par défaut).
  • Une application avec un consentement OpenIddict External ne peut pas utiliser ce mode : aucune autorisation permanente ne correspond à un identifiant aléatoire.

Fonctions indisponibles​

FonctionComportement
CreateUserAsync, réinitialisation et changement de mot de passeNotSupportedException.
MFARefusée au démarrage (voir MFA).
Clients OAuth demandant offline_accessLe scope est retiré : pas de jeton de rafraîchissement. Le client Blazor de Lagoon renouvelle ses jetons sans lui.
Profils de grilles Remote personnelsLgSettings.UserId a une clé étrangère vers AspNetUsers : utilisez le mode Local (par défaut, stockage dans le navigateur). Les profils partagés fonctionnent.
Notifications (Lagoon.UI.Notifications)NotificationUserWithUser.UserId a une clé étrangère vers AspNetUsers : l'insertion échoue.
Entités de l'application liées à l'utilisateurToute clé étrangère vers la table des utilisateurs échoue de la même façon.
CGU (acceptation par utilisateur)À éviter : l'acceptation est liée à l'identifiant de session. Les CGU sont redemandées à chaque connexion et chaque acceptation écrit une ligne dans EulaAcceptances.

Les tables Identity restent dans le modèle de données : elles sont créées par les migrations mais restent vides.