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
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
InvalidOperationExceptionest levée siAllowFormAuthenticationest vrai, ou siMultiFactorAuthenticationRequiredouMultiFactorAuthenticationRoleNamesont 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
OnSignInouOnSignInAsyncaveccontext.AddCustomRole(...).IsCustomRolesEnabledest forcé àtrueet ne peut pas être remis àfalse(InvalidOperationException). Sans rôle, la connexion est refusée (AuthNoAppAccess).
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
| Étape | Mode Database | Mode None |
|---|---|---|
| Recherche de l'utilisateur | FindByEmailAsync / FindByNameAsync | Aucune. GetIdentityUserAsync et OnUnknownUserAsync ne sont pas appelés. |
Utilisateur passé à OnSignIn(Async) | Utilisateur de la base | Utilisateur transitoire, jamais attaché au DbContext : UserName = login, Email = login (connexion par e-mail) ou claim e-mail du fournisseur. |
| Rôles, claims, logins externes | Enregistrés en base | Non enregistrés. Les jetons du fournisseur restent dans le cookie. |
Identifiant (sub) | AspNetUsers.Id | Guid aléatoire à chaque connexion : rien ne relie deux sessions d'une même personne. |
| Cookie | Glissant, security stamp relu en base toutes les 30 minutes | Expiration absolue (SsoAuthenticationCookieLifeTime), aucune relecture. |
| Codes et jetons OpenIddict | Principal reconstruit depuis la base, codes enregistrés dans OpenIddictTokens | Principal copié depuis le cookie, aucune ligne dans OpenIddictTokens. |
| Autorisation OpenIddict | Permanent | AdHoc (identifiant aléatoire, client, scopes), supprimée par la purge Quartz. |
| Jeton de rafraîchissement | Émis si le client demande offline_access | Jamais émis : le scope offline_access est retiré. |
| Reconnexion (popup) | Comparaison des identifiants en base | Le login du fournisseur doit être celui du cookie de la session. |
Conséquences
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 claimDisplayName.- 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
Externalne peut pas utiliser ce mode : aucune autorisation permanente ne correspond à un identifiant aléatoire.
Fonctions indisponibles
| Fonction | Comportement |
|---|---|
CreateUserAsync, réinitialisation et changement de mot de passe | NotSupportedException. |
| MFA | Refusée au démarrage (voir MFA). |
Clients OAuth demandant offline_access | Le scope est retiré : pas de jeton de rafraîchissement. Le client Blazor de Lagoon renouvelle ses jetons sans lui. |
Profils de grilles Remote personnels | LgSettings.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'utilisateur | Toute 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.