Modern SaaS and desktop software often requires secure license key generation. While many teams rely on third-party licensing providers, building your own system in ASP.NET Core gives you full control over:
Key format
Activation limits
Expiration & renewal
Machine binding
Offline validation
Logging & tracking
This article shows how to create a professional-grade license key generator and validator using ASP.NET Core 8, including database schema, services, and a secure signing mechanism.
1. Requirements and Design Goals
Your custom licensing system should support:
Key generation
Unique, unpredictable license keys
Expiration date
Optional device/machine binding
Usage count or activation limit
Optional edition-level restrictions (Basic, Pro, Enterprise)
Key validation API
Security
Use asymmetric cryptography (RSA 2048+)
License keys must be tamper-proof
Server holds private key; client uses public key
Ease of export
2. Database Structure (SQL Server)
License Table
CREATE TABLE Licenses (
LicenseId UNIQUEIDENTIFIER PRIMARY KEY,
CustomerName NVARCHAR(200),
Email NVARCHAR(200),
LicenseKey NVARCHAR(500),
Edition NVARCHAR(50),
ExpirationDate DATETIME,
MaxActivations INT,
CreatedOn DATETIME DEFAULT GETDATE()
);
Activation Table
CREATE TABLE LicenseActivations (
ActivationId UNIQUEIDENTIFIER PRIMARY KEY,
LicenseId UNIQUEIDENTIFIER,
MachineId NVARCHAR(200),
ActivatedOn DATETIME DEFAULT GETDATE()
);
3. License Payload Structure
Before generating a license key, create a payload object that describes the license.
public class LicensePayload
{
public Guid LicenseId { get; set; }
public string Edition { get; set; }
public DateTime ExpirationDate { get; set; }
public int MaxActivations { get; set; }
public string CustomerName { get; set; }
public string Email { get; set; }
}
This payload will be serialized to JSON → signed with RSA → encoded as Base64 → distributed as Key.
4. RSA Key Pair (One-time setup)
Generate RSA keys once:
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private.pem -out public.pem
Store private.pem securely (do NOT commit to Git).
Clients will use public.pem to validate offline.
5. License Signing Service (ASP.NET Core)
Load RSA keys
public class RsaKeyService
{
private readonly RSA _privateKey;
private readonly RSA _publicKey;
public RsaKeyService(IWebHostEnvironment env)
{
_privateKey = RSA.Create();
_privateKey.ImportFromPem(File.ReadAllText(Path.Combine(env.ContentRootPath, "Keys/private.pem")));
_publicKey = RSA.Create();
_publicKey.ImportFromPem(File.ReadAllText(Path.Combine(env.ContentRootPath, "Keys/public.pem")));
}
public RSA PrivateKey => _privateKey;
public RSA PublicKey => _publicKey;
}
6. Generating a License Key
public class LicenseGenerator
{
private readonly RsaKeyService _rsa;
public LicenseGenerator(RsaKeyService rsa)
{
_rsa = rsa;
}
public string GenerateLicense(LicensePayload payload)
{
string json = JsonSerializer.Serialize(payload);
// sign the payload
byte[] data = Encoding.UTF8.GetBytes(json);
byte[] signature = _rsa.PrivateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// combine payload + signature
var licenseFile = new
{
Payload = payload,
Signature = Convert.ToBase64String(signature)
};
string finalJson = JsonSerializer.Serialize(licenseFile);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(finalJson));
}
}
This results in a compact Base64 license key.
7. License Validation API (Server-side)
Clients call this API to validate their key.
[HttpPost("validate")]
public async Task<IActionResult> Validate([FromBody] string licenseKey)
{
var json = Encoding.UTF8.GetString(Convert.FromBase64String(licenseKey));
var obj = JsonSerializer.Deserialize<LicenseFile>(json);
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(obj.Payload));
byte[] signatureBytes = Convert.FromBase64String(obj.Signature);
bool valid = _rsa.PublicKey.VerifyData(
payloadBytes,
signatureBytes,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
if (!valid)
return Unauthorized("Invalid license key");
// Check expiration
if (obj.Payload.ExpirationDate < DateTime.UtcNow)
return Unauthorized("License expired");
// Check activation limit
int count = await _db.LicenseActivations
.CountAsync(a => a.LicenseId == obj.Payload.LicenseId);
if (count >= obj.Payload.MaxActivations)
return Unauthorized("Activation limit reached");
return Ok("Valid");
}
8. Activation Registration (Server-side)
When the client first activates:
[HttpPost("activate")]
public async Task<IActionResult> Activate(ActivationRequest req)
{
// validate license key first
// ... (same as previous section)
_db.LicenseActivations.Add(new LicenseActivation
{
ActivationId = Guid.NewGuid(),
LicenseId = obj.Payload.LicenseId,
MachineId = req.MachineId
});
await _db.SaveChangesAsync();
return Ok("Activation successful");
}
9. Client-side Offline Validation (C#)
Client apps should validate the license locally even without internet:
public bool ValidateOffline(string licenseKey, string publicKeyPem)
{
var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
var json = Encoding.UTF8.GetString(Convert.FromBase64String(licenseKey));
var obj = JsonSerializer.Deserialize<LicenseFile>(json);
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(obj.Payload));
byte[] signatureBytes = Convert.FromBase64String(obj.Signature);
bool valid = rsa.VerifyData(
payloadBytes,
signatureBytes,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
return valid && obj.Payload.ExpirationDate > DateTime.UtcNow;
}
10. Generating a Printable License File (JSON)
var licenseJson = Encoding.UTF8.GetString(Convert.FromBase64String(licenseKey));
File.WriteAllText("license.json", licenseJson);
Clients can import this file for offline verification.
11. Enhancements for Production
Add the following for enterprise-level robustness:
License revocation list
Encrypted machine IDs (SHA256)
License renewal workflow
Webhook for usage reporting
Admin dashboard to manage licenses
Export audit logs
Multi-tenant licensing for SaaS
Hardware-bound fingerprinting
12. Multi-Environment Licensing (Dev, QA, UAT, Prod)
In real-world enterprise applications, licensing usually differs across environments.
To support this, your license payload and validation services can include an Environment attribute that restricts where a license can be used.
Why it’s needed
Prevent developers from using production-grade licenses in dev machines
Allow QA teams to simulate expiration, upgrades, revocations
Provide controlled testing environments for partners
Add to LicensePayload
public string Environment { get; set; } // Dev, QA, UAT, Prod
Validation rule
if (obj.Payload.Environment != currentEnvironment)
return Unauthorized("License not valid for this environment");
This enables strict environment-based validation and prevents misuse of production licenses.
13. Rate-Limited License Validation API (Anti-Abuse Layer)
When products validate frequently (e.g., every startup), it may create opportunities for:
Adding rate limiting ensures controlled usage.
ASP.NET Core Built-in Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("licenseValidation", limiterOptions =>
{
limiterOptions.PermitLimit = 20;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 5;
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
});
Apply to validation endpoint
[EnableRateLimiting("licenseValidation")]
[HttpPost("validate")]
public IActionResult ValidateLicense([FromBody] string licenseKey)
{
// validation logic
}
14. Conclusion
This guide demonstrates a full, secure, scalable license key system using ASP.NET Core. It includes key generation, RSA signing, secure storage, activation tracking, and online/offline validation.