Skip to content
← Tous les articles
· Delta1 Labs Licensing.NETGuide

Licence par utilisateur nommé : lier les droits à une personne, pas à une machine

La liaison à la machine répond à « quel est cet ordinateur ? » — mais beaucoup de produits se vendent au *siège*, pas à la boîte. Un développeur avec un portable, un poste de bureau et un runner de CI est un utilisateur payant, pas trois activations. La licence par utilisateur nommé inverse l'ancrage : la licence appartient à une identité, et les appareils pendent sous elle comme des feuilles, sous un plafond que vous contrôlez. Voici comment modéliser un droit d'identité signé dans Keyright, échanger une connexion contre un bail (lease) de courte durée limité à l'appareil, imposer une limite d'appareils sans fixer l'utilisateur à une seule boîte, et laisser un utilisateur retirer un appareil d'un siège plein — avec le vrai C# pour l'émettre, le lier et le vérifier.

La plupart des conseils sur la licence partent de la machine : prenez l’empreinte de l’appareil, liez-y la licence, et désormais une activation égale une boîte. Ce modèle est propre et c’est le bon défaut pour beaucoup de logiciels de bureau. Mais il suppose en silence quelque chose souvent faux : qu’un client est une machine. Vendez un outil de développement et votre client est une personne qui a un portable, un poste de bureau au travail et un agent de CI qui exécute le même outil dans un conteneur. En node-locking, ce seul développeur payant brûle trois activations et ouvre un ticket de support la première semaine. L’unité que vous avez réellement vendue était un siège, et un siège appartient à une personne.

La licence par utilisateur nommé rend cela explicite. La licence est ancrée à une identité — un e-mail, un id de sujet stable — et les appareils pendent sous l’identité comme des enfants plafonnés et révocables. Le développeur se connecte sur trois machines ; le serveur remet à chacune un bail de courte durée limité à l’appareil ; le siège reste à un. Cet article détaille la modélisation de cela dans Keyright : à quoi ressemble le droit d’identité signé, comment une connexion devient un bail d’appareil, comment la limite d’appareils est imposée sans fixer qui que ce soit à une seule boîte, et comment un utilisateur retire un appareil obsolète d’un siège plein.

Deux ancres, pas une

Le changement de point de vue consiste à cesser de penser « la licence » comme un unique blob signé lié au matériel, et à commencer à penser à deux artefacts signés aux durées de vie distinctes. Le droit est lié à l’identité de l’utilisateur et vit aussi longtemps que l’abonnement. Le bail d’appareil est lié à un appareil sous cette identité, est de courte durée, et c’est ce que l’application en cours d’exécution vérifie réellement à chaque démarrage.

droit (utilisateur)sub=alex@acme.dev · cap=3vit pour la durée de l'abonnementbail d'appareildev=laptop-9f3 · 12hsigné, id dans les claimsbail d'appareildev=desk-21a · 12hsigné, id dans les claimsbail d'appareildev=ci-7b2 · 12hsigné, id dans les claims4e appareil → refuséplafond plein jusqu'à libération

Le droit est ce que vous vendez et renouvelez. Le bail d’appareil est une attestation dérivée et jetable. Parce que le bail est de courte durée et signé avec l’id de l’appareil à l’intérieur, vous obtenez la révocation presque gratuitement : cessez d’émettre des baux pour un appareil et il cessera de fonctionner au prochain renouvellement, sans jamais intervenir dans le client en cours d’exécution.

Le droit d’identité signé

Keyright signe une charge canonique ; pour la licence par utilisateur nommé, l’ancre de la charge est le sujet (sub) plutôt qu’une empreinte de machine. Une charge de droit minimale ressemble à ceci :

public sealed record Entitlement
{
    public required string Sub { get; init; }        // stable user id, e.g. "usr_7Yq..." (not the raw email)
    public required string Product { get; init; }     // "acme-tool"
    public required string Tier { get; init; }         // "Pro"
    public required int DeviceCap { get; init; }        // seats-per-user: 3
    public required DateTimeOffset NotAfter { get; init; } // subscription end
    public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
}

Deux choix délibérés. D’abord, Sub est un id opaque et stable — pas l’e-mail brut. Les e-mails changent ; un id de sujet non, et garder l’e-mail hors du claim signé signifie que vous ne réémettez pas de droits chaque fois que quelqu’un change d’adresse. Ensuite, DeviceCap vit dans le droit signé, donc la limite voyage avec la licence et ne peut pas être modifiée côté client. Le serveur lit le plafond dans le droit vérifié quand il décide d’émettre ou non un autre bail d’appareil.

Le bail d’appareil est une seconde charge, signée séparément, qui référence le droit :

public sealed record DeviceLease
{
    public required string Sub { get; init; }        // same subject as the entitlement
    public required string Device { get; init; }      // device id derived from the fingerprint
    public required string Product { get; init; }
    public required string Tier { get; init; }
    public required DateTimeOffset NotAfter { get; init; } // SHORT: hours/days
    public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
}

L’application en cours d’exécution ne vérifie jamais qu’un DeviceLease. Elle ne voit jamais directement le terme de l’abonnement ; elle voit une courte expiration qui la force à revenir au serveur périodiquement, où le droit de longue durée est revérifié.

Transformer une connexion en bail d’appareil

L’appareil calcule une empreinte (le même matériau d’identité machine que vous utiliseriez pour le node-locking), la condense en un id d’appareil compact et demande un bail au serveur. La requête porte l’identité authentifiée de l’utilisateur et l’id de l’appareil ; la réponse est un DeviceLease signé ou un refus.

public async Task<LeaseResult> RequestLeaseAsync(
    AuthenticatedUser user, CancellationToken ct)
{
    // Device id is a stable, non-reversible hash of the fingerprint inputs.
    string deviceId = DeviceId.Compute(MachineFingerprint.Collect());

    var resp = await _client.PostAsJsonAsync("v1/lease", new LeaseRequest
    {
        Product = "acme-tool",
        Device  = deviceId
    }, ct); // user identity travels on the authenticated channel, not in the body

    if (resp.StatusCode == HttpStatusCode.Conflict)
    {
        // Cap is full: the server tells us which devices hold the slots
        // so the user can choose one to release.
        var full = await resp.Content.ReadFromJsonAsync<SeatFullResponse>(ct);
        return LeaseResult.SeatFull(full!.Devices);
    }

    resp.EnsureSuccessStatusCode();
    var lease = await resp.Content.ReadFromJsonAsync<SignedLease>(ct);
    return LeaseResult.Issued(lease!);
}

Côté serveur, le point de terminaison du bail est l’endroit où les deux ancres se rencontrent. Il résout le droit pour le sujet authentifié, vérifie que l’abonnement est actif, puis impose le plafond d’appareils en comptant les appareils enregistrés distincts, réémettant pour un appareil qui détient déjà un emplacement plutôt que d’en consommer un nouveau :

public async Task<IResult> IssueLease(LeaseRequest req, ClaimsPrincipal caller)
{
    string sub = caller.RequireSubject();
    Entitlement ent = await _entitlements.GetActiveAsync(sub, req.Product)
        ?? return Results.Forbid(); // no live subscription → no lease

    // A device that already holds a slot just gets a fresh lease (idempotent).
    DeviceRegistration? existing = await _devices.FindAsync(sub, req.Product, req.Device);
    if (existing is null)
    {
        int inUse = await _devices.CountAsync(sub, req.Product);
        if (inUse >= ent.DeviceCap)
            return Results.Conflict(await _devices.DescribeAsync(sub, req.Product));

        await _devices.RegisterAsync(sub, req.Product, req.Device, DateTimeOffset.UtcNow);
    }
    else
    {
        await _devices.TouchAsync(existing.Id, DateTimeOffset.UtcNow); // last-seen
    }

    DeviceLease lease = new()
    {
        Sub      = sub,
        Device   = req.Device,
        Product  = ent.Product,
        Tier     = ent.Tier,
        Features = ent.Features,
        NotAfter = DateTimeOffset.UtcNow.AddHours(12)   // short, independent clock
    };

    return Results.Ok(_signer.Sign(lease)); // Keyright canonical-payload signing
}

Deux propriétés en découlent. Le point de terminaison est idempotent par appareil : un appareil qui revient réémet au lieu de consommer un emplacement, donc renouveler un bail ne coûte jamais un siège. Et le plafond est imposé contre le DeviceCap signé, lu dans le droit que le serveur a résolu — le client n’a jamais voix au chapitre sur le nombre d’appareils qu’il peut enregistrer.

Ce que vérifie le client

L’application vérifie le bail d’appareil comme elle vérifierait n’importe quelle licence Keyright — elle récupère la clé publique du signataire, vérifie la signature sur les octets canoniques, et seulement ensuite lit les claims. La seule étape spécifique à l’utilisateur nommé est qu’elle contrôle l’id d’appareil du bail contre cet appareil, pour qu’un bail émis pour une machine ne puisse pas être rejoué sur une autre, même sous le même compte :

public LicenseState Evaluate(SignedLease signed)
{
    if (!_verifier.TryVerify(signed, out DeviceLease lease))
        return LicenseState.Invalid("signature");

    // The lease is bound to a device; confirm it is *this* device.
    string thisDevice = DeviceId.Compute(MachineFingerprint.Collect());
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(lease.Device),
            Encoding.UTF8.GetBytes(thisDevice)))
        return LicenseState.Invalid("device-mismatch");

    if (lease.NotAfter <= DateTimeOffset.UtcNow)
        return LicenseState.Expired; // refresh against the server

    return LicenseState.Active(lease.Tier, lease.Features);
}

L’expiration ici est celle du bail d’appareil — l’horloge courte. Quand elle expire, le client se renouvelle ; le renouvellement réexécute le contrôle de droit du serveur, donc un abonnement expiré ou un appareil retiré produit un refus et l’application retombe à son état sans licence à la prochaine frontière. Vous n’avez jamais à pousser un signal d’arrêt dans un client en cours d’exécution ; le bail court est l’interrupteur d’arrêt, et le silence du serveur est ce qui le déclenche.

Retirer un appareil d’un siège plein

Un plafond d’appareils n’est humain que si l’utilisateur peut libérer un emplacement sans ticket de support. Parce que chaque bail met à jour un horodatage « vu pour la dernière fois », la réponse de siège plein peut lister les appareils de l’utilisateur avec assez de contexte pour en choisir un à retirer :

public async Task<IResult> ReleaseDevice(string device, ClaimsPrincipal caller)
{
    string sub = caller.RequireSubject();

    // Cooldown stops a shared account from churning devices to fake extra seats.
    if (await _devices.RemovedRecentlyAsync(sub, TimeSpan.FromMinutes(30)))
        return Results.StatusCode(StatusCodes.Status429TooManyRequests);

    await _devices.RemoveAsync(sub, "acme-tool", device);
    return Results.NoContent();
}

Le délai de récupération est le détail qui garde le plafond significatif. Sans lui, un compte partagé pourrait retirer et rajouter des appareils toutes les quelques secondes et faire tourner, de fait, des machines illimitées ; avec un plancher de 30 minutes sur les retraits, le plafond devient un vrai plafond et le partage de compte nuit à celui qui partage — sa propre machine suivante est bloquée — bien plus qu’à vous. Le bail en cours de l’appareil retiré continue de fonctionner jusqu’à sa courte expiration, puis ne peut plus se renouveler car son emplacement a disparu. Pas de coupure brutale, pas de siège bloqué.

À retenir

Le node-locking demande « quelle est cette machine ? » et c’est la bonne question pour un logiciel vendu à la boîte. La licence par utilisateur nommé demande « à qui est ce siège ? » — la bonne question quand vous vendez à des personnes qui travaillent légitimement sur plusieurs machines. Ancrez le droit à un id de sujet stable avec le plafond d’appareils dans les claims signés, dérivez des baux d’appareil de courte durée qui portent l’id de l’appareil pour qu’ils ne puissent pas être rejoués, et imposez le plafond côté serveur en comptant les appareils enregistrés distincts avec réémission idempotente pour qu’un renouvellement ne coûte jamais un siège. Donnez aux utilisateurs un retrait d’appareil en libre-service avec un délai de récupération, et laissez le bail court être votre mécanisme de révocation pour ne jamais avoir à intervenir dans un client en cours d’exécution. Faites cela et un développeur avec un portable, un poste de bureau et un agent de build est exactement ce qu’il a payé pour être : un siège. Pour la moitié « identité d’appareil » de tout ceci — comment l’empreinte elle-même est construite — voyez empreinte de machine et liaison d’appareil, et pour la mécanique de charge signée sous les deux ancres, l’anatomie d’une clé de licence signée.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.