Skip to content
← Alle Beiträge
· Delta1 Labs LizenzierungBerechtigungenASP.NET CoreLeitfaden

Eine Lizenz ist ein Capability-Token: Keyright-Berechtigungen als ASP.NET-Core-Autorisierungsrichtlinien

Die meisten Anwendungen prüfen die Lizenz einmal beim Start und verstreuen IsEnabled-Aufrufe im gesamten Code. In einem ASP.NET-Core-Dienst ist das ein Fehler: Sie haben bereits eine Autorisierungs-Pipeline, die genau die Frage „Darf dieser Aufrufer das tun?" beantwortet, und eine lizenzierte Berechtigung ist exakt diese Frage. Hier zeigen wir, wie Sie Keyright-Berechtigungen auf das Autorisierungssystem abbilden: eine Requirement, die eine Berechtigung benennt, ein Handler, der die validierte Lizenz liest, ein Policy-Provider, der Richtlinien bei Bedarf erzeugt, und [Authorize(Policy = "ent:export")] auf den Endpunkten, die es brauchen — alles fail-closed, alles offline.

Eine Desktop-Anwendung kann es sich leisten, ihre Lizenz einmal beim Start zu prüfen und ein paar Booleans umzuschalten. Ein Dienst nicht. Anfragen treffen nebenläufig ein, Endpunkte haben unterschiedliche Capability-Anforderungen, und „Ist dieser Aufruf erlaubt?” ist eine Frage, die Sie ohnehin bei jeder Anfrage beantworten — genau dafür gibt es die Autorisierungs-Pipeline von ASP.NET Core. Wenn eine Funktion durch eine Lizenz geschützt ist, ist die lizenzierte Berechtigung nur eine weitere Eingabe in dieselbe Frage. if (license.IsEnabled("export")) über Ihre Controller zu verstreuen erfindet — schlecht — ein System neu, das Sie bereits haben.

Dieser Beitrag verdrahtet Keyright-Berechtigungen mit der ASP.NET-Core-Autorisierung, sodass eine lizenzierte Capability zu einer Richtlinie wird, die Sie deklarativ anhängen:

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

Kein if im Körper. Enthält die Lizenz kein export, erreicht die Anfrage Export nie — sie bekommt ein 403 von der Pipeline, bevor Ihr Code läuft.

Die drei Bausteine

Die ASP.NET-Core-Autorisierung hat eine Form, die sich sauber auf Berechtigungen abbilden lässt. Eine Requirement ist ein schlichtes Datenobjekt, das beschreibt, was wahr sein muss. Ein Handler entscheidet, ob eine Requirement für die aktuelle Anfrage erfüllt ist. Eine Richtlinie (Policy) ist ein benanntes Bündel von Requirements, auf das [Authorize(Policy = …)] verweist. Wir brauchen eine Requirement, die einen Berechtigungsnamen trägt, einen Handler, der die validierte Keyright-Lizenz liest, und — weil wir nicht pro Funktion von Hand eine Richtlinie registrieren wollen — einen Policy-Provider, der die Richtlinie erzeugt, sobald zum ersten Mal eine ent:<name>-Richtlinie angefordert wird.

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

Schritt 1 — eine Requirement, die eine Berechtigung benennt

Die Requirement ist reine Daten. Sie trägt den Berechtigungsschlüssel, den der Handler nachschlagen wird.

using Microsoft.AspNetCore.Authorization;

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

Das ist der gesamte Typ. Er hält keine Lizenz und trifft keine Entscheidung — er sagt nur, um welche Berechtigung es in dieser Richtlinie geht.

Schritt 2 — ein Handler, der die validierte Lizenz liest

Im Handler verrichtet das Keyright-SDK seine Arbeit. Registrieren Sie Ihren KeyrightClient beim Start als Singleton (er lädt den öffentlichen Schlüssel einmal und kann sicher geteilt werden), injizieren Sie ihn, und lesen Sie in HandleRequirementAsync die validierte Lizenz und rufen Sie context.Succeed nur auf, wenn die Berechtigung aktiv ist. Tun Sie auf dem Verweigerungspfad nichts — die Requirement unerfüllt zu lassen ist genau das, was das 403 erzeugt.

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;
    }
}

Zwei Eigenschaften des Keyright-SDK machen das sicher. Validate() wirft nie eine Exception — bei jedem Fehlschlag gibt es ein LicenseInfo in der Free-Edition zurück, das den Grund trägt — der Handler kann die Pipeline also nicht zum Absturz bringen. Und IsEnabled arbeitet fail-closed — eine unbekannte oder fehlende Berechtigung wird als deaktiviert gelesen. Der Verweigerungspfad ist damit das Standardverhalten, nichts, was Sie defensiv programmieren müssen.

Validate() ist reine lokale Berechnung, der Aufruf pro Anfrage ist also günstig; wenn Sie aber vermeiden wollen, die Signatur bei jedem Aufruf erneut zu prüfen, können Sie das LicenseInfo hinter einem kurzen Zeitfenster zwischenspeichern und dem Handler den Cache statt des Clients übergeben — die Entscheidung bleibt in beiden Fällen vollständig offline.

Schritt 3 — ein Policy-Provider, damit Sie nicht jede Funktion von Hand registrieren

Sie könnten in Program.cs einmal pro Funktion AddPolicy("ent:export", …) aufrufen, aber das heißt, den Start jedes Mal anzufassen, wenn Sie eine neue Capability schützen. Ein Policy-Provider baut die Richtlinie bei Bedarf aus dem Namen. Jede Richtlinie der Form ent:<entitlement> wird zu einer Richtlinie mit einer einzigen 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();
}

Weil der Provider RequireAuthenticatedUser() mit der Berechtigungs-Requirement kombiniert, bedeutet ent:export bereits authentifiziert und für Export lizenziert. Unbekannte Namen an den DefaultAuthorizationPolicyProvider zu delegieren hält Ihre rollen- und schemabasierten Richtlinien unverändert funktionsfähig.

Verdrahten Sie die drei Bausteine in 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();

Jetzt ist [Authorize(Policy = "ent:export")] auf jedem beliebigen Endpunkt ein vollständiges, deklaratives Lizenz-Gate.

Numerische Limits sind ebenfalls eine Requirement

Flags sind die einfache Hälfte. Berechtigungen tragen auch numerische Limits — Seats, Projekte, Anfragen — und die entscheiden sich an der aktuellen Nutzung, nicht an einem statischen Ja/Nein. Modellieren Sie das als zweite Requirement, deren Handler GetLimit mit einem Nutzungszähler vergleicht, den Sie injizieren:

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 gibt für ein fehlendes oder nicht parsbares Limit Ihren konservativen fallback zurück und für das Literal "unlimited" einen Maximalwert, sodass ein unlizenzierter Aufrufer auf 1 gedeckelt ist und ein unbegrenzter Plan nie blockiert. Da jede Requirement einer Richtlinie bestehen muss, können Sie authentifiziert, für Projekte lizenziert und unter der Projektobergrenze in einem einzigen Attribut verlangen, indem Sie den Provider so erweitern, dass er etwa limit:max-projects erkennt.

Warum das die richtige Nahtstelle ist

Berechtigungsprüfungen in die Autorisierung zu legen ist nicht nur aufgeräumter — es ändert den Fehlermodus. Bei verstreuten if-Prüfungen bedeutet eine vergessene Prüfung, dass eine Funktion stillschweigend unlizenziert ausgeliefert wird; der Standard ist erlauben. Mit einer Richtlinie am Endpunkt fällt ein vergessenes Attribut im Review laut auf, und sobald es da ist, ist der Standard verweigern — die Anfrage wird abgelehnt, bevor Ihre Action läuft, mit einem sauberen 403, das Ihr Client einheitlich behandeln kann. Die Entscheidung bleibt offline und fail-closed, genau wie überall sonst in Keyright, sie kombiniert sich mit den Rollen und Schemata, die Sie bereits durchsetzen, und eine neue geschützte Funktion hinzuzufügen ist ein Attribut plus höchstens eine Zeile Konfiguration. Die Lizenz hört auf, etwas zu sein, das Sie prüfen, und wird zu einer Capability, die die Anfrage entweder trägt oder nicht.

Siehe den Leitfaden zum Feature-Gating für das Berechtigungsmodell, auf dem das hier aufbaut, und fail-closed vs. fail-open dafür, warum der Verweigern-als-Standard-Pfad wichtig ist.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.