Skip to content
← Tous les articles
· Delta1 Labs LicencesDroitsASP.NET CoreGuide

Une licence est un jeton de capacité : les droits Keyright comme politiques d'autorisation ASP.NET Core

La plupart des applications vérifient la licence une fois au démarrage et dispersent des appels IsEnabled dans tout le code. Dans un service ASP.NET Core, c'est une erreur : vous disposez déjà d'un pipeline d'autorisation conçu pour répondre à « cet appelant a-t-il le droit de faire cela ? », et un droit sous licence est exactement cette question. Voici comment projeter les droits Keyright sur le système d'autorisation : une exigence qui nomme un droit, un handler qui lit la licence validée, un fournisseur de politiques qui les crée à la demande, et [Authorize(Policy = "ent:export")] sur les endpoints qui en ont besoin — le tout fail-closed, le tout hors ligne.

Une application de bureau peut se permettre de vérifier sa licence une fois au démarrage et de basculer quelques booléens. Un service, non. Les requêtes arrivent de façon concurrente, les endpoints ont des exigences de capacité différentes, et « cet appel est-il autorisé ? » est une question à laquelle vous répondez déjà à chaque requête — c’est précisément à cela que sert le pipeline d’autorisation d’ASP.NET Core. Lorsqu’une fonctionnalité est protégée par une licence, le droit sous licence n’est qu’une entrée de plus dans cette même question. Disperser if (license.IsEnabled("export")) dans vos contrôleurs réinvente, et mal, un système que vous possédez déjà.

Cet article branche les droits Keyright sur l’autorisation ASP.NET Core afin qu’une capacité sous licence devienne une politique que vous attachez de manière déclarative :

[HttpPost("export")]
[Authorize(Policy = "ent:export")]
public IActionResult Export([FromBody] ExportRequest req) => Ok(_exporter.Run(req));

Aucun if dans le corps. Si la licence ne contient pas export, la requête n’atteint jamais Export — elle reçoit un 403 du pipeline, avant que votre code ne s’exécute.

Les trois pièces

L’autorisation ASP.NET Core a une forme qui se projette proprement sur les droits. Une exigence est un simple objet de données décrivant ce qui doit être vrai. Un handler décide si une exigence est satisfaite pour la requête courante. Une politique est un ensemble nommé d’exigences auquel [Authorize(Policy = …)] fait référence. Il nous faut une exigence qui porte un nom de droit, un handler qui lit la licence Keyright validée et — parce que nous ne voulons pas enregistrer à la main une politique par fonctionnalité — un fournisseur de politiques qui fabrique la politique la première fois qu’une politique ent:<name> est demandée.

RequestPOST /exportAuthorizationresolve "ent:export"Entitlement handlerreads validated licenseKeyright SDK — offline Validate(), no networkEndpoint runs403 Forbiddenhas itlacks it

Étape 1 — une exigence qui nomme un droit

L’exigence est de la pure donnée. Elle porte la clé du droit que le handler ira chercher.

using Microsoft.AspNetCore.Authorization;

public sealed class EntitlementRequirement : IAuthorizationRequirement
{
    public EntitlementRequirement(string entitlement) => Entitlement = entitlement;
    public string Entitlement { get; }
}

C’est tout le type. Il ne contient aucune licence et ne prend aucune décision — il dit seulement de quel droit cette politique traite.

Étape 2 — un handler qui lit la licence validée

Le handler est l’endroit où le SDK Keyright fait son travail. Enregistrez votre KeyrightClient en singleton au démarrage (il charge la clé publique une seule fois et peut être partagé en toute sécurité), injectez-le et, dans HandleRequirementAsync, lisez la licence validée et n’appelez context.Succeed que lorsque le droit est activé. Ne faites rien sur le chemin de refus — laisser l’exigence non satisfaite est ce qui produit le 403.

using Microsoft.AspNetCore.Authorization;

public sealed class EntitlementHandler : AuthorizationHandler<EntitlementRequirement>
{
    private readonly KeyrightClient _license;

    public EntitlementHandler(KeyrightClient license) => _license = license;

    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, EntitlementRequirement requirement)
    {
        // Offline: verifies the signature, expiry, node-lock and trial/clock state
        // against the embedded public key. Never throws; fails closed to Free.
        LicenseInfo info = _license.Validate();

        if (info.IsValid && info.Entitlements.IsEnabled(requirement.Entitlement))
            context.Succeed(requirement);

        // No else. A missing/expired/revoked license or a disabled entitlement
        // leaves the requirement unmet → 403.
        return Task.CompletedTask;
    }
}

Deux propriétés du SDK Keyright rendent cela sûr. Validate() ne lève jamais d’exception — en cas d’échec, quel qu’il soit, il renvoie un LicenseInfo en édition Free portant la raison — le handler ne peut donc pas faire tomber le pipeline. Et IsEnabled échoue en mode fermé — un droit inconnu ou absent est lu comme désactivé. Le chemin de refus est donc le comportement par défaut, pas quelque chose que vous codez de manière défensive.

Validate() est un calcul purement local, l’appeler à chaque requête est donc peu coûteux ; mais si vous voulez éviter de revérifier la signature à chaque appel, vous pouvez mettre le LicenseInfo en cache derrière une courte fenêtre de temps et donner le cache au handler à la place du client — la décision reste entièrement hors ligne dans les deux cas.

Étape 3 — un fournisseur de politiques pour ne pas enregistrer chaque fonctionnalité à la main

Vous pourriez appeler AddPolicy("ent:export", …) une fois par fonctionnalité dans Program.cs, mais cela signifie modifier le démarrage chaque fois que vous protégez une nouvelle capacité. Un fournisseur de politiques construit la politique à la demande à partir du nom. Toute politique de la forme ent:<entitlement> devient une politique avec une seule EntitlementRequirement("<entitlement>") :

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

public sealed class EntitlementPolicyProvider : IAuthorizationPolicyProvider
{
    private const string Prefix = "ent:";
    private readonly DefaultAuthorizationPolicyProvider _fallback;

    public EntitlementPolicyProvider(IOptions<AuthorizationOptions> options)
        => _fallback = new DefaultAuthorizationPolicyProvider(options);

    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(Prefix, StringComparison.Ordinal))
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()                                   // compose with auth
                .AddRequirements(new EntitlementRequirement(policyName[Prefix.Length..]))
                .Build();
            return Task.FromResult<AuthorizationPolicy?>(policy);
        }
        // Any non-entitlement policy name falls through to the normal provider.
        return _fallback.GetPolicyAsync(policyName);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync();
    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync();
}

Comme le fournisseur compose RequireAuthenticatedUser() avec l’exigence de droit, ent:export signifie déjà authentifié et sous licence pour l’export. Déléguer les noms inconnus au DefaultAuthorizationPolicyProvider conserve vos politiques basées sur les rôles et les schémas intactes.

Câblez les trois pièces dans Program.cs :

builder.Services.AddSingleton(KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-service",
    PublicKeyBase64 = builder.Configuration["Keyright:PublicKey"],   // not a secret; verify-only
    ServiceUrl      = builder.Configuration["Keyright:ServiceUrl"],
}));

builder.Services.AddSingleton<IAuthorizationPolicyProvider, EntitlementPolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, EntitlementHandler>();
builder.Services.AddAuthorization();

Désormais, [Authorize(Policy = "ent:export")] sur n’importe quel endpoint est une barrière de licence complète et déclarative.

Les limites numériques sont aussi une exigence

Les drapeaux sont la moitié facile. Les droits portent aussi des limites numériques — sièges, projets, requêtes — et celles-ci se jugent sur l’utilisation courante, pas sur un oui/non statique. Modélisez cela comme une seconde exigence dont le handler compare GetLimit à un compteur d’utilisation que vous injectez :

public sealed class WithinLimitRequirement(string limit) : IAuthorizationRequirement
{
    public string Limit { get; } = limit;
}

public sealed class WithinLimitHandler(KeyrightClient license, IUsageCounter usage)
    : AuthorizationHandler<WithinLimitRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, WithinLimitRequirement requirement)
    {
        long cap = license.GetLimit(requirement.Limit, fallback: 1);   // fail-closed fallback
        if (usage.Current(requirement.Limit) < cap)
            context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

GetLimit renvoie votre fallback conservateur pour une limite absente ou non analysable, et une valeur maximale pour le littéral "unlimited", de sorte qu’un appelant sans licence est plafonné à 1 et qu’un forfait illimité ne bloque jamais. Comme chaque exigence d’une politique doit passer, vous pouvez exiger authentifié, sous licence pour les projets et sous le plafond de projets en un seul attribut en étendant le fournisseur pour qu’il reconnaisse, par exemple, limit:max-projects.

Pourquoi c’est la bonne couture

Placer les vérifications de droits dans l’autorisation n’est pas seulement plus propre — cela change le mode de défaillance. Avec des vérifications if dispersées, en oublier une signifie qu’une fonctionnalité est livrée silencieusement sans licence ; le comportement par défaut est autoriser. Avec une politique sur l’endpoint, oublier l’attribut échoue bruyamment en revue, et une fois qu’il est en place, le comportement par défaut est refuser — la requête est rejetée avant que votre action ne s’exécute, avec un 403 propre que votre client peut traiter uniformément. La décision reste hors ligne et fail-closed exactement comme partout ailleurs dans Keyright, elle se compose avec les rôles et schémas que vous appliquez déjà, et ajouter une nouvelle fonctionnalité protégée revient à un attribut plus, au maximum, une ligne de configuration. La licence cesse d’être une chose que vous vérifiez et devient une capacité que la requête porte, ou ne porte pas.

Consultez le guide de protection des fonctionnalités pour le modèle de droits sur lequel tout cela repose, et fail-closed contre fail-open pour comprendre pourquoi le chemin de refus par défaut est important.

Essayez Nebula.NET

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