Activating Machines
Use Keygen's API and your code to implement machine activation for your product. If you need help after reading this, can reach out to us anytime at [email protected].
This tutorial is written in JavaScript, but you can follow along in any
other language that you're comfortable in. Throughout this tutorial, you
will see placeholders such as {ACCOUNT}
within the code examples that
will need to be replaced with an ID for that particular resource type.
Before getting started, you will either need to log into your Dashboard and generate a product token, or generate an authentication token via the API. To create a token directly through the API, check out the code examples in the API reference or check out our guide on authenticating users.
Once you've created a license, you can "activate" individual machines for your product to be used on, making it easier for you to implement e.g. a node-locked licensing model. Although Keygen doesn't use the term "activation" much in our resource documentation, machine creation accomplishes the same thing, while machine deletion would be "deactivation."
Implementing a node-locked or node-limited license model consists of 3 to 4 separate steps, each done in a separate API request:
- Create the license - this is the step where you create the user's license resource, typically done server-side after you've received a payment, or after they've registered for an account.
- Validate their license - this step should be initiated client-side from within your software (e.g. prompt them for a license key after starting your software), and can be performed both before and after activation. Validating before activation so you can be sure the license supports another machine; validating after activation to ensure the newest activation didn't invalidate the license.
- Activate their machine - this step should be initiated from within your software, so you can have access to the underlying machine to be able to query information such as MAC address, HDD ID, etc. for use in fingerprinting the user's current machine.
Activation requests can either be performed client- or server-side, and depending on which one of those you choose, the tokens you use to make the request will be different i.e. product tokens vs. user tokens vs. activation tokens, for server-side and client-side machine activations.
We have examples of server-side implementations that you can use as a reference for implementing your own license activation system. We have example servers written in Node, as well as in PHP.
In order to perform machine activation client-side, you will need to be authenticated as a user or use an activation token which belongs to the license you're activating a machine for.
Embedding your admin or product tokens within your software is not secure.
When activating a machine, you will at minimum need a license ID for the license the activation is for, a fingerprint for the current device, as well as an optional user ID for client-side activation. Other machine attributes, such as IP or hostname, are completely optional.
In the example below, we'll be using a SHA512
hash of the current device's
unique identifer as a fingerprint. We're hashing the user's device ID as a
simple way to anonymize it, since we have no real use in knowing its actual
value. Using an HMAC
digest (i.e. HMAC-SHA512
) might be an even
better way to anonymize the user's MAC
address.
Although we're using a device's unique machine ID in this example, a fingerprint can be any unique string able to be reproducibly queried from the device, e.g. you could write a secure random string to a file on disk or to a registry and use that, but for example purposes the device's ID works well enough.
Fingerprinting the device
const { machineId } = require('node-machine-id')
const crypto = require('crypto')
const getFingerprint = async () => {
const id = await machineId({ original: true })
return crypto.createHash('sha512')
.update(id)
.digest('hex')
}
Next up, we'll use our getFingerprint
function above while creating a
new machine resource that is associated with the current user
and their license. Before activating a
machine, you should validate the current license
to make sure its policy will allow for an additional machine activation.
You can utilize a policy's maxMachines
, floating
and concurrent
attributes to enforce your desired rules for machine usage. For example,
you can create a policy which allows up to 3
machines to be activated
per-license by setting maxMachines=3
, floating=true
, and concurrent=false
.
You can check how many machines a license is allowed to have through
its maxMachines
attribute, which is synced with the license's policy.
You can also query all of a license's machines through its machines
relationship. These can be useful for managing UI state, e.g. not
showing an "activate machine" action if the user is already at their
machine activation limit.
Activating the machine
Below is an example of performing the machine activation request.
const fetch = require("node-fetch")
const response = await fetch("https://api.keygen.sh/v1/accounts/{ACCOUNT}/machines", {
method: "POST",
headers: {
"Content-Type": "application/vnd.api+json",
"Accept": "application/vnd.api+json",
"Authorization": "Bearer {ACTIVATION_TOKEN}"
},
body: JSON.stringify({
"data": {
"type": "machines",
"attributes": {
"fingerprint": await getFingerprint()
},
"relationships": {
"license": {
"data": { "type": "licenses", "id": "{LICENSE}" }
}
}
}
})
})
const { data: machine, errors } = await response.json()
if (errors) {
// … handle errors
}
console.log(`Machine activated: ${machine.attributes.fingerprint}`)
scheme
attribute. Alternatively, check out our example client/server implementation of offline license
activation for air-gapped machines
using Keygen, Node.js, React, TOTP, QR codes, and a mobile device.
Example Activation Flow
Below is a full example of how to implement a robust machine activation flow, following the above mentioned steps. You may of course choose to implement a different flow if needed - this only serves as an example implementation.
View example of activation flow
async function activateLicense(key, fingerprint) {
// Validate the license key before activation, so we can be sure it supports
// another machine. Notice that this validation is scoped to the current
// machine via its fingerprint - this ensures that license activation is
// not performed for machines that are already activated.
const validation = await fetch('https://api.keygen.sh/v1/accounts/{ACCOUNT}/licenses/actions/validate-key', {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'
},
body: JSON.stringify({
meta: {
scope: { fingerprint },
key
}
})
})
const { meta, data: license } = await validation.json()
// If the license is valid, that means the current machine is already
// activated. We can safely return.
if (meta.valid) {
return null
}
// If we've gotten this far, our license is not valid for the current
// machine and we should attempt to activate it.
switch (meta.constant) {
// This means the license already has at least 1 machine associated with
// it, but none match the current machine's fingerprint. We're breaking
// on this case because, for this example, we want to support activating
// more than 1 machine.
case 'FINGERPRINT_SCOPE_MISMATCH':
// You will receive a NO_MACHINES status when the license IS floating,
// and when it does not currently have any associated machines.
case 'NO_MACHINES':
// You will receive a NO_MACHINE status when the license IS NOT floating
// i.e. it's node-locked, and when it does not currently have any
// associated machines.
case 'NO_MACHINE': {
break
}
default: {
throw new Error(`Activation failed: ${meta.detail} (${meta.constant})`)
}
}
// Attempt to activate the current machine for the license, using the
// license ID that we received from the validation response and the
// current machine's fingerprint.
const activation = await fetch('https://api.keygen.sh/v1/accounts/{ACCOUNT}/machines', {
method: 'POST',
headers: {
'Authorization': 'Bearer {PRODUCT_TOKEN}',
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'
},
body: JSON.stringify({
data: {
type: 'machines',
attributes: {
fingerprint
},
relationships: {
license: {
data: { type: 'licenses', id: license.id }
}
}
}
})
})
const { data: machine, errors } = await activation.json()
if (errors) {
throw new Error(errors.map(e => `${e.title}: ${e.detail}`).join(', '))
}
// All is good - the machine was successfully activated.
return machine
}
async function main() {
const fingerprint = await getFingerprint()
const key = await getLicenseKey()
try {
const machine = await activateLicense(key, fingerprint)
if (machine == null) {
console.log('The current machine has already been activated!')
return
}
console.log('The current machine was successfully activated!', machine)
} catch(err) {
console.error('Activating the current machine failed', err)
}
}
main()
Next steps
That's that! You've activated your first machine using Keygen. Next up,
we could update a license's policy to require a machine fingerprint
during validating using the requireFingerprintScope
setting.
If you have any questions about what you've learned today, be sure
to reach out!