Skip to content
← Todas las publicaciones
· Delta1 Labs LicenciamientoDerechosASP.NET CoreGuía

Una licencia es un token de capacidad: derechos de Keyright como políticas de autorización de ASP.NET Core

La mayoría de las aplicaciones comprueban la licencia una vez al arrancar y dispersan llamadas a IsEnabled por todo el código. En un servicio ASP.NET Core eso es un error: ya tienes un pipeline de autorización construido para responder "¿puede este llamador hacer esto?", y un derecho licenciado es exactamente esa pregunta. Aquí explicamos cómo proyectar los derechos de Keyright sobre el sistema de autorización: un requisito que nombra un derecho, un handler que lee la licencia validada, un proveedor de políticas que las crea bajo demanda y [Authorize(Policy = "ent:export")] en los endpoints que lo necesitan — todo fail-closed, todo sin conexión.

Una aplicación de escritorio puede permitirse comprobar su licencia una vez al arrancar y activar unos cuantos booleanos. Un servicio no. Las peticiones llegan de forma concurrente, los endpoints tienen requisitos de capacidad distintos, y “¿está permitida esta llamada?” es una pregunta que ya estás respondiendo en cada petición: para eso existe el pipeline de autorización de ASP.NET Core. Cuando una funcionalidad está protegida por una licencia, el derecho licenciado es simplemente una entrada más a esa misma pregunta. Dispersar if (license.IsEnabled("export")) por tus controladores reinventa, y mal, un sistema que ya tienes.

Este artículo conecta los derechos de Keyright con la autorización de ASP.NET Core para que una capacidad licenciada se convierta en una política que adjuntas de forma declarativa:

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

Sin if en el cuerpo. Si la licencia no incluye export, la petición nunca llega a Export: recibe un 403 del pipeline, antes de que se ejecute tu código.

Las tres piezas

La autorización de ASP.NET Core tiene una forma que encaja limpiamente con los derechos. Un requisito es un objeto de datos plano que describe lo que debe cumplirse. Un handler decide si un requisito se cumple para la petición actual. Una política es un conjunto con nombre de requisitos al que hace referencia [Authorize(Policy = …)]. Necesitamos un requisito que lleve el nombre de un derecho, un handler que lea la licencia validada de Keyright y — como no queremos registrar a mano una política por funcionalidad — un proveedor de políticas que fabrique la política la primera vez que se solicite una política ent:<name>.

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

Paso 1 — un requisito que nombra un derecho

El requisito es puro dato. Lleva la clave del derecho que el handler buscará.

using Microsoft.AspNetCore.Authorization;

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

Ese es todo el tipo. No contiene ninguna licencia ni toma ninguna decisión: solo dice de qué derecho trata esta política.

Paso 2 — un handler que lee la licencia validada

El handler es donde el SDK de Keyright hace su trabajo. Registra tu KeyrightClient como singleton al arrancar (carga la clave pública una vez y es seguro compartirlo), inyéctalo y, en HandleRequirementAsync, lee la licencia validada y llama a context.Succeed solo cuando el derecho está activado. No hagas nada en la ruta de denegación: dejar el requisito sin satisfacer es lo que produce el 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;
    }
}

Dos propiedades del SDK de Keyright hacen que esto sea seguro. Validate() nunca lanza excepciones — ante cualquier fallo devuelve un LicenseInfo en la edición Free con el motivo — así que el handler no puede hacer caer el pipeline. E IsEnabled falla cerrado: un derecho desconocido o ausente se lee como deshabilitado. La ruta de denegación es, por tanto, el comportamiento predeterminado, no algo que programes a la defensiva.

Validate() es cómputo puramente local, así que llamarlo por petición es barato; pero si quieres evitar volver a verificar la firma en cada llamada, puedes cachear el LicenseInfo durante una ventana de tiempo corta y pasarle al handler la caché en lugar del cliente — la decisión sigue siendo completamente offline en ambos casos.

Paso 3 — un proveedor de políticas para no registrar cada funcionalidad a mano

Podrías llamar a AddPolicy("ent:export", …) una vez por funcionalidad en Program.cs, pero eso implica editar el arranque cada vez que proteges una nueva capacidad. Un proveedor de políticas construye la política bajo demanda a partir del nombre. Cualquier política con la forma ent:<entitlement> se convierte en una política con un único 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();
}

Como el proveedor compone RequireAuthenticatedUser() con el requisito de derecho, ent:export ya significa autenticado y licenciado para exportar. Delegar los nombres desconocidos al DefaultAuthorizationPolicyProvider mantiene intactas tus políticas basadas en roles y esquemas.

Conecta las tres piezas en 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();

Ahora [Authorize(Policy = "ent:export")] en cualquier endpoint es una puerta de licencia completa y declarativa.

Los límites numéricos también son un requisito

Los flags son la mitad fácil. Los derechos también llevan límites numéricos — asientos, proyectos, peticiones — y esos se evalúan contra el uso actual, no contra un sí/no estático. Modélalo como un segundo requisito cuyo handler compara GetLimit con un contador de uso que inyectas:

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 devuelve tu fallback conservador para un límite ausente o no analizable, y un valor máximo para el literal "unlimited", de modo que un llamador sin licencia queda limitado a 1 y un plan ilimitado nunca bloquea. Como todos los requisitos de una política deben pasar, puedes exigir autenticado, licenciado para proyectos y por debajo del tope de proyectos en un único atributo extendiendo el proveedor para que reconozca, por ejemplo, limit:max-projects.

Por qué esta es la costura correcta

Poner las comprobaciones de derechos en la autorización no es solo más ordenado: cambia el modo de fallo. Con comprobaciones if dispersas, olvidar una significa que una funcionalidad se entrega silenciosamente sin licencia; el valor predeterminado es permitir. Con una política en el endpoint, olvidar el atributo falla de forma visible en la revisión, y una vez que está, el valor predeterminado es denegar: la petición se rechaza antes de que se ejecute tu acción, con un 403 limpio que tu cliente puede manejar de manera uniforme. La decisión sigue siendo offline y fail-closed exactamente como en el resto de Keyright, se compone con los roles y esquemas que ya aplicas, y añadir una nueva funcionalidad protegida es un atributo más, como mucho, una línea de configuración. La licencia deja de ser algo que compruebas y se convierte en una capacidad que la petición lleva o no lleva.

Consulta la guía de protección de funcionalidades para el modelo de derechos sobre el que se construye esto, y fail-closed frente a fail-open para entender por qué importa la ruta de denegación por defecto.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.