Building a robust software licensing system requires careful consideration of cryptographic security, attack vectors, and implementation details. While many licensing systems rely on simple API calls or basic key validation, creating a more secure system demands a deeper understanding of cryptographic principles and secure communication patterns.
At its core, a secure licensing system must solve several fundamental challenges. How do we ensure that validation responses haven't been tampered with? How can we prevent replay attacks where valid responses are captured and reused? This article will present a robust solution to these challenges using cryptographic signatures, nonce validation, and secure key management. However, it's important to acknowledge an uncomfortable truth: no licensing system is truly unbreakable. Since license validation code must ultimately run on untrusted machines, a determined attacker could modify the software to bypass these checks entirely. Our goal, therefore, is to implement security measures that make bypass attempts impractical and time-consuming, while providing a frictionless experience for legitimate users. We'll focus on preventing the most common attack vectors — response tampering and replay attacks— while keeping our implementation clean and maintainable.
The security approach
Understanding the attack surface is crucial for building effective defenses. When a client application validates a license, it typically sends a request to a validation server and receives a response indicating whether the license is valid. Without proper security measures, attackers can exploit this process in several ways. They might intercept and modify the server's response, turning a "license invalid" message into "license valid." They could record a valid response and replay it later, bypassing the need for a real license key. Or they might reverse engineer the client's validation logic to understand and circumvent it.
Our solution addresses these vulnerabilities through multiple layers of security. At its foundation, we use RSA public-key cryptography to sign all server responses. The validation server holds the private key and signs each response with it, while the client applications contain only the public key. This means that while clients can verify that a response came from our server, they cannot generate valid responses themselves. Even if an attacker intercepts and modifies a response, the signature verification will fail.
To prevent replay attacks, we implement a nonce system. Each license check generates a unique random value (the nonce) that must be included in both the request and the signed response. The client verifies that the response contains the exact nonce it generated, ensuring that old responses cannot be reused. This effectively turns each license check into a unique cryptographic challenge that must be solved by the server.
Architecture
Our licensing system is built around a secure API that handles license validation requests, with the architecture designed to support our security requirements. The system consists of three main components: the validation API, the license storage, and the client function.
Validation API: Implemented as an Azure Function, providing a serverless endpoint that handles license verification requests. We chose Azure Functions for several reasons: they offer excellent security features like managed identities for accessing other Azure services, built-in HTTPS enforcement, and automatic scaling based on demand. The validation endpoint is stateless, with each request containing all necessary information for validation, making it highly reliable and easy to scale.
License storage: We use Azure Cosmos DB, which provides several advantages for our use case. First, it offers strong consistency guarantees, ensuring that license status changes are immediately reflected across all regions. Second, it includes built-in encryption at rest and in transit, adding an additional security layer to our sensitive license data. Third, its flexible schema allows us to easily store different types of licenses and associated metadata.
Client function: I'll be writing my client function in JavaScript, though it is easily replicable in the language that is most relevant for your product. It handles nonce generation, request signing, and response verification, encapsulating these security details behind a clean, simple interface that application developers can easily integrate.
Let's look at how this fits together with a flowchart illustrating the journey:
Prerequisites
To follow along this blog, you'll need the following:
- Azure subscription: You will need an active subscription to host the services we will use. Make sure you have appropriate permissions to create and manage resources. If you don't have one, you can sign up here: https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account
- Visual Studio Code: For the development environment, install Visual Studio Code. Other IDEs are available, though we will be benefiting from the Azure Functions extension within this IDE. Download VS Code here: https://code.visualstudio.com/
- VS Code Azure Functions extension (optional): There are many different ways to deploy to Azure Functions, but having one-click deploy functionality inside our IDE is extremely useful. To install on VS Code, head to Extensions > Search Azure Functions > Click Install.
Building the solution
Generating a public/private key pair
As outlined, we'll be using RSA cryptography as a security measure. This means that we'll need a public and private key pair. There are many ways to generate these, but the one I will suggest does it all locally through the terminal, so there is no risk of exposing the private key.
Run the following command in a local terminal:
ssh-keygen -t rsa -b 2048
Then follow the wizard through. It will save the files at C:\Users\your-user\.ssh. You are expecting to find a id_rsa and a id_rsa.pub. Both of these can be opened via Notepad or other text editors.
The .pub file type represents the public key, and the other is the private key. It is extremely important to keep the private key absolutely confidential, while there is no risk associated with exposing the public key. If the private key was exposed, then attackers could sign responses as if it were the server to circumvent license checks.
Keep these keys handy as we will need them for signing and verifying signatures for this system.
Deploying the resources
Before we dive into the implementation, we need to set up our Azure infrastructure. Our system requires two main components: an Azure Function to host our validation endpoint and a Cosmos DB instance to store our license data.
Let's start with Cosmos DB. From the Azure Portal, create a new Cosmos DB account. Select the Azure Cosmos DB for NoSQL API - while Cosmos DB supports multiple APIs, this option provides the simplicity and flexibility we need for our license storage. During creation, you can select the serverless pricing tier if you're expecting low traffic, or provisioned throughput for more predictable workloads. Make note of your endpoint URL and primary key - we'll need these later for our function configuration.
Once your Cosmos DB account is created, create a new database named licensing and a container named licenses. For the partition key, use /id since we'll be using the license key itself as both the document ID and partition key. This setup ensures efficient lookups when validating licenses.
Next, let's deploy our Azure Function. Create a new Function App from the portal, selecting Node.js as your runtime stack and the newest version available. Choose the Consumption (Serverless) plan type - this way, you only pay for actual usage and get automatic scaling. Make sure you're using the same region as your Cosmos DB to minimize latency.
After the Function App is created, you'll need to configure your application settings. Navigate to the Configuration section and add the following settings:
- COSMOS_ENDPOINT: Your Cosmos DB endpoint URL
- COSMOS_KEY: Your Cosmos DB primary key
- PRIVATE_KEY: Your RSA private key for signing responses
You may also want to consider setting up Application Insights for monitoring. It's particularly useful for tracking license validation patterns and detecting potential abuse attempts.
With these resources deployed, we're ready to implement our validation system. The next sections will cover the actual code implementation for both the server and client components.
Creating the validation API
As discussed, the validation API will be deployed as an Azure Function. While this use case only requires one endpoint, the benefit is that it is scalable with the ability to add more endpoints as the scope widens.
To create a Function project using the Azure Functions extension:
Go to Azure tab > Click the Azure Functions icon > Click Create New Project > Choose a folder > Choose JavaScript > Choose Model V4 > Choose HTTP trigger > Provide a name (eg 'validation') > Click Open in new window.
This will have created and opened a brand new Function project, with a HTTP trigger. This HTTP trigger will represent our validation endpoint and will be what the client calls to get a signed verdict on the license key provided by the user.
Now let's write the code for this validation endpoint. The base trigger provided by the setup process should look something like this:
const { app } = require('@azure/functions');
app.http('validate', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
context.log(`Http function processed request for url "${request.url}"`);
const name = request.query.get('name') ||
await request.text() ||
'world';
return {
body: `Hello, ${name}!`
};
}
});
Let's make a few modifications to this base:
- Ensure that the only allowed HTTP method is POST, as there is no need to support both and we will make use of the request body allowed in POST requests.
- Clear everything inside that function to make way for our upcoming code.
- Optional: The first parameter inside the http function, where mine is validate, represents the route that will be used. With the current setup, mine would be example.com/validate. If you want to change this path, now is the time by adjusting that parameter.
Now, let's work forward from this adjusted base:
const { app } = require('@azure/functions');
app.http('validate', {
methods: ['POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
}
});
Our job now is handle an incoming validation request. As per the above flow chart, we must check the license key against actual existing keys stored in our Cosmos DB database, sign the response with the private key and then return it to the client.
First, let's define the format that we expect in the incoming body and give ourselves access to that data. This validation endpoint should expect two data points: licenseKey and nonce. The following code will pull those two variables from the body, and return an error in the case that they have not been provided:
try {
const body = await request.json();
const { licenseKey, nonce } = body;
if (!licenseKey) {
return {
status: 400,
body: { error: 'Missing licenseKey' }
};
}
if (!nonce) {
return {
status: 400,
body: { error: 'Missing nonce' }
};
}
...
} catch (error) {
return {
status: 400,
body: { error: 'Invalid JSON in request body' }
};
}
Now that we know for sure a license key and nonce were provided, this is just a case of adding layers of checks.
The first check to add is to ensure that the license key and nonce formats are valid. It makes sense to have a standardized format, such as a UUID, so we can reduce unnecessary database calls. I'll use UUID format. It's important to remember the desired format, so that we can (a) give frontend validation to the end user if they provide an wrongly formatted license key, and (b) ensure we generate a correctly formatted nonce.
As for validating this, add the following regex as a const at the top of the class:
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
Then add these two checks after our previous ones:
if (!UUID_REGEX.test(licenseKey)) {
return {
status: 400,
body: { error: 'Invalid license key format' }
};
}
if (!UUID_REGEX.test(nonce)) {
return {
status: 400,
body: { error: 'Invalid nonce format' }
};
}
Now, we are validating the format of both the license key and nonce using a UUID regular expression pattern. The code uses the test method to check if each string matches the expected UUID format (like "123e4567-e89b-4d3c-8456-426614174000"). If either the license key or nonce doesn't match this pattern, the function returns an 400 Bad Request response with a specific error message indicating which field had the invalid format. This validation ensures that we only process requests where both the license key and nonce are properly formatted UUIDs.
Next is the most important check: validating the license. As explained, I'm choosing to use Azure Cosmos DB for this, though this part may vary depending on your chosen data store. Before we add any code, let's first conceptualize a basic schema for our data. Something like this:
{
"type": "object",
"required": [
"id",
"redeemed"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
},
"redeemed": {
"type": "boolean",
"default": false
}
}
}
Cosmos DB is a great solution for this because we can easily add to the schema as the scope changes. For example, some potential ways to increase this functionality are:
- An expiry date
- A set of all activations, enforcing a cap
- Data to identify the license holder once redeemed
Now that we know what data to search, let's add the code. First, we need to install the required dependency so we can use Cosmos DB:
> npm install azure/cosmos
Next, create a local.settings.json file if it doesn't already exist. Then, add two environment variables to the Values section, COSMOS_ENDPOINT and COSMOS_KEY:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "node",
"COSMOS_ENDPOINT": "X",
"COSMOS_KEY": "X"
}
}
Be sure to populate those variables with the actual data from your deployed Cosmos DB resource. We'll be using these in a moment to programmatically connect to it.
Add this import to the top of the function class:
const { CosmosClient } = require('@azure/cosmos');
Now, let's add the code to connect to our Cosmos DB resource and read the data to validate the license:
const cosmosClient = new CosmosClient({
endpoint: process.env.COSMOS_ENDPOINT,
key: process.env.COSMOS_KEY
});
const database = cosmosClient.database('licensing');
const container = database.container('licenses');
try {
const { resource: license } = await container.item(licenseKey, licenseKey).read();
if (!license) {
return {
status: 404,
body: { error: 'License not found' }
};
}
...
} catch (dbError) {
if (dbError.code === 404) {
return {
status: 404,
body: { error: 'License not found' }
};
}
return {
status: 500,
body: { error: 'Error validating license' }
};
}
The first part of the code creates our connection to Cosmos DB. We use the CosmosClient class from the SDK, providing it with our database endpoint and key from environment variables. Then we specify we want to use the licensing database and the licenses container within it where our license documents are stored.
The core validation logic is surprisingly simple. We use the container's item method to look up the license directly using the provided license key. We use this key as both the item ID and partition key, which makes our lookups very efficient - it's like looking up a word in a dictionary rather than reading through every page.
If no license is found, we return a 404 status code with a clear error message. We've also implemented proper error handling that distinguishes between a license not existing (404) and other potential database errors (500). This gives clients clear feedback about what went wrong while keeping our error messages secure - we don't expose internal system details that could be useful to attackers. Also, at this point we don't need to do any cryptographic signing, because there is no benefit to an attacker in replaying a rejected validation.
Now, after confirming a license exists in our database, but before returning it to the client, we need to sign our response to prevent tampering. This is crucial for security - it ensures that responses can't be modified or forged, as they need a valid signature that can only be created with our private key and verified with our public key.
The signing process uses Node's built-in crypto module for RSA signing. First, we load our private key from environment variables. Then, for any valid license response, we create a SHA-256 signature of the JSON data which we provide in the HTTP response.
Add this import to the top of the class:
const crypto = require('crypto');
Then, add your previously generated private key to our environment variables:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "node",
"COSMOS_ENDPOINT": "X",
"COSMOS_KEY": "X",
"PRIVATE_KEY": "X"
}
}
Then, after our Cosmos DB lookup, add the following code to sign a valid response:
const respData = {
valid: !license.redeemed,
nonce,
licenseKey
};
const sign = crypto.createSign('SHA256');
const dataString = JSON.stringify(respData);
sign.update(dataString);
return {
body: {
...respData,
signature: sign.sign(process.env.PRIVATE_KEY, 'base64')
}
};
Now, every valid response includes both the license data and a cryptographic signature of that data. The client can then verify this signature using our public key to ensure the response hasn't been tampered with.
Finally, after we have created respData, and set valid to the inverse of license.redeemed, let's ensure we set redeemed to true in the database, so that the key cannot be used for future requests. Between signing the JSON and returning the data, add the following code:
await container.item(licenseKey, licenseKey).patch([
{
op: 'replace',
path: '/redeemed',
value: true
}
]);
This ensures a license can only be validated once and marks it as redeemed upon first use.
And that's it for the validation endpoint! We've built a secure HTTP endpoint that validates license keys through a series of steps. It first checks that the incoming request contains both a license key and nonce in the correct UUID format. Then it looks up the license in our Cosmos DB to verify its existence and redemption status. Finally, for valid licenses, it returns a cryptographically signed response that includes the license status and the original nonce. The signature ensures our responses can't be tampered with, while the nonce prevents replay attacks. Our error handling provides clear feedback without exposing sensitive system details, making the endpoint both secure and developer-friendly.
The final code for this endpoint should look like this:
const { app } = require('@azure/functions');
const { CosmosClient } = require('@azure/cosmos');
const crypto = require('crypto');
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
app.http('validate', {
methods: ['POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
try {
const body = await request.json();
const { licenseKey, nonce } = body;
if (!licenseKey) {
return {
status: 400,
body: { error: 'Missing licenseKey' }
};
}
if (!nonce) {
return {
status: 400,
body: { error: 'Missing nonce' }
};
}
if (!UUID_REGEX.test(licenseKey)) {
return {
status: 400,
body: { error: 'Invalid license key format' }
};
}
if (!UUID_REGEX.test(nonce)) {
return {
status: 400,
body: { error: 'Invalid nonce format' }
};
}
const cosmosClient = new CosmosClient({
endpoint: process.env.COSMOS_ENDPOINT,
key: process.env.COSMOS_KEY
});
const database = cosmosClient.database('licensing');
const container = database.container('licenses');
try {
const { resource: license } = await container.item(licenseKey, licenseKey).read();
if (!license) {
return {
status: 404,
body: { error: 'License not found' }
};
}
const respData = {
valid: !license.redeemed,
nonce,
licenseKey
};
const sign = crypto.createSign('SHA256');
const dataString = JSON.stringify(respData);
sign.update(dataString);
await container.item(licenseKey, licenseKey).patch([
{
op: 'replace',
path: '/redeemed',
value: true
}
]);
return {
body: {
...respData,
signature: sign.sign(process.env.PRIVATE_KEY, 'base64')
}
};
} catch (dbError) {
if (dbError.code === 404) {
return {
status: 404,
body: { error: 'License not found' }
};
}
return {
status: 500,
body: { error: 'Error validating license' }
};
}
} catch (error) {
return {
status: 400,
body: { error: 'Invalid JSON in request body' }
};
}
}
});
Adding the client function
All of this server-side validation is pointless unless we properly verify responses on the client side. Our signed responses and nonce system are security features that only work if we actually validate them. Imagine if someone intercepted the server response and modified it to always return valid: true - without verification, our client would happily accept this fraudulent response. Similarly, without nonce checking, someone could capture a valid response and replay it later, effectively reusing a one-time license multiple times.
Two critical checks need to happen when we get a response from our validation endpoint:
- We need to verify that the response's signature is valid using our public key. This ensures the response actually came from our server and wasn't tampered with in transit. Even the smallest modification to the response data would cause the signature verification to fail.
- We must confirm that the nonce in the response matches the one we generated for this specific request. The nonce is like a unique ticket - it should only be valid for one specific validation attempt. This prevents replay attacks where someone could capture a valid response and reuse it later.
As outlined earlier, this client check could be written with any programming language in any environment that allows a HTTP request. For consistency, I'll do mine in JavaScript, though know that there would be no issues in porting this over to Java, Kotlin, C++, C#, Go, etc. This function would then be implemented into your product and triggered when the user provides a license key that needs validating.
Let's start with a base function, with crypto imported ready for the signature verification and the licenseKey parameter provided by the user:
const crypto = require('crypto');
async function validateLicense(licenseKey) {
}
Before we make our request to the validation server, we need to generate a unique nonce that we'll use to prevent replay attacks. Using the crypto module we just imported, we can generate a UUID for this purpose:
const nonce = crypto.randomUUID();
Now we can make our request to the validation endpoint:
try {
const response = await fetch('https://your-function-url/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
licenseKey,
nonce
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'License validation failed');
}
...
} catch (error) {
console.error('License validation failed:', error.message);
return false;
}
With our nonce generated, we make our HTTP request to the validation server. We send a POST request with our license key and nonce in the request body, making sure to set the Content-Type header to indicate we're sending JSON data. The response handling is thorough but straightforward: if we get anything other than a successful response (indicated by response.ok being false), we attempt to parse the error message from the response body and throw an error. For successful responses, we parse the JSON data which will contain our validation result, along with the nonce and cryptographic signature we'll need to verify. This gives us a clean way to handle both successful validations and various error cases (like invalid license keys or server errors) while ensuring we have proper data to perform our security checks.
Now, let's check the nonce:
if (data.nonce !== nonce) {
return false;
}
This simple but crucial comparison verifies that the response we received was actually generated for our specific request. By checking data.nonce !== nonce, we ensure the nonce returned by the server exactly matches the one we generated. If there's any mismatch, we return false immediately - we don't even bother checking the signature or license status because a nonce mismatch indicates either a replay attack (someone trying to reuse an old valid response) or response tampering. Think of it like a ticket number at a deli counter - if you give them ticket #45 but they call out #46, something's wrong and you need to stop right there.
Now we perform our second and most crucial security check: cryptographic signature verification. Using Node's crypto module, we create a SHA256 verifier and reconstruct the exact same data structure that the server signed - this includes the validation result, nonce, and license key in a specific order. We verify this data against the signature provided in the response using our public key. The server signed this data with its private key, and only the matching public key can verify it - any tampering with the response data would cause this verification to fail. If the signature is valid, we've confirmed two things: the response definitely came from our validation server (not an impersonator) and the data hasn't been modified in transit. Only then do we trust and return the validation result.
Let's add the code after the nonce check which checks the signature:
const verifier = crypto.createVerify('SHA256');
verifier.update(JSON.stringify({
valid: data.valid,
nonce: data.nonce,
licenseKey: data.licenseKey
}));
const isSignatureValid = verifier.verify(
process.env.PUBLIC_KEY,
data.signature,
'base64'
);
if (!isSignatureValid) {
return false;
}
return data.valid;
The final code for this function should look like this:
const crypto = require('crypto');
async function validateLicense(licenseKey) {
const nonce = crypto.randomUUID();
try {
const response = await fetch('https://your-function-url/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
licenseKey,
nonce
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'License validation failed');
}
const data = await response.json();
if (data.nonce !== nonce) {
return false;
}
const verifier = crypto.createVerify('SHA256');
verifier.update(JSON.stringify({
valid: data.valid,
nonce: data.nonce,
licenseKey: data.licenseKey
}));
const isSignatureValid = verifier.verify(
process.env.PUBLIC_KEY,
data.signature,
'base64'
);
if (!isSignatureValid) {
return false;
}
return data.valid;
} catch (error) {
console.error('License validation failed:', error.message);
return false;
}
}
With our validation function complete, we can now reliably check license keys from any part of our application. The function is designed to be straightforward to use while handling all the security checks under the hood - just pass in a license key and await the result. It returns a simple boolean: true if the license is valid and verified, false for any kind of failure (invalid license, network errors, security check failures). Here's how you might use it:
const isValid = await validateLicense('123e4567-e89b-4d3c-8456-426614174000');
if (isValid) {
// License is valid - enable features, start your app, etc.
} else {
// License is invalid - show error, disable features, etc.
}
Testing
Let's do a couple tests running this function.
Firstly, I'll do a valid license key that I've inserted into the database. The document looks like this:
{
"id": "123e4567-e89b-4d3c-8456-426614174000",
"redeemed": false
}
Here's the response:
{
"valid": true,
"nonce": "987fcdeb-51a2-4321-9b54-326614174000",
"licenseKey": "123e4567-e89b-4d3c-8456-426614174000",
"signature": "XkxZWR0eHpKTFE3VVhZT3JUEtYeXNJWUZ6SWJoTUtmMX0JhVXBHK1ZzN2lZYzdGSnJ6SEJa1VOCnBrWkhDU0xGS1ZFTDmFZN1BUeUlHRzl1V0tJPT0="
}
Running the function returns true. Which means that the above response has passed the nonce and signature validation too!
I tested with a license key which does not exist in the database, 0bd84e7b-d91e-47e8-81b8-f39a5c1f8c72, and the result was that the function returned false.
Summary
Building a secure license validation system is a delicate balance between security and usability. The implementation we've created provides strong security guarantees through cryptographic signatures and nonce validation, while remaining straightforward to integrate into any application.
Let's recap the key security features we've implemented:
- UUID-based license keys and nonces for standardized formatting
- Server-side validation using Azure Cosmos DB for efficient license lookups
- Response signing using RSA public-key cryptography
- Nonce verification to prevent replay attacks
- Comprehensive error handling with secure error messages
While this system provides robust protection against common attack vectors like response tampering and replay attacks, it's important to remember that no licensing system is completely unbreakable. Since validation code must run on untrusted machines, a determined attacker could potentially modify the software to bypass these checks entirely. Our goal is to make bypass attempts impractical and time-consuming while providing a frictionless experience for legitimate users.
The architecture we've chosen - using Azure Functions and Cosmos DB - gives us plenty of room to grow. Some potential enhancements could include:
- Rate limiting to prevent brute force attempts
- IP-based activation limits
- License expiration dates
- Feature-based licensing tiers
- Usage analytics and reporting
The modular nature of our implementation means adding these features would require minimal changes to the core validation logic. Our schema-less database choice means we can evolve our license data structure without disrupting existing functionality.
Remember to store your private key securely and never expose it in client-side code. The public key used for verification can be distributed with your client applications, but the private key should be treated as a critical secret and stored securely in your Azure configuration.
This concludes our journey through building a secure license validation system. While there's always room for additional security measures and features, this implementation provides a solid foundation that can be built upon as your needs evolve.
Updated Jan 26, 2025
Version 4.0StephenMS
Microsoft
Joined May 28, 2024
Apps on Azure Blog
Follow this blog board to get notified when there's new activity