Ever since the release of the Plugin Update Checker library, one of the most common questions asked has been this: “How can I secure my download links so that only users who have purchased my plugin can download an update?”
In this post I’ll try to answer that question in the context of using WP Update Server to serve plugin updates. I will also provide a couple of practical examples from one of my commercial plugins.
Lets get started. There are many ways to secure update downloads, but most of them boil down to this:
- Give some kind of security token to each user. This could be a login + password to a membership site, a license key, or something more esoteric.
- Whenever someone installs your plugin on their site, ask them to enter the key/log in/whatever.
- Modify the plugin to append the token to each update request.
- Write a server script that verifies the token before it allows download.
Choose a Security Token
How you implement the first two steps will vary greatly depending on your plugin UI and what online store, cart software, or membership plugin you use. You might already have some kind of customer authentication mechanism in place that just needs a bit of tweaking to be used for updates, or you might need to build your own from scratch. There’s no “one size fits all” solution here.
Personally, I prefer to use license keys. Whenever someone buys my Admin Menu Editor Pro plugin, the order processing script generates a random key, stores it in the database, and sends the key and a download link to the customer’s email. Then, when they install the plugin, there’s a link to enter the license key.
I won’t include the license management code here because it’s beyond the scope of this post and built for that specific plugin, but the user interface looks something like this (click to enlarge):
Add the Token To Update Requests
Now, how do we add our security token to each update request? You can do that by using the addQueryArgFilter($callback)
method of the update checker. The callback function will receive an associative array of query arguments. Just add the token to the list and return the modified array.
Here’s an example:
/* ... Code that initializes the update checker ... */
//Add the license key to query arguments.
$updateChecker->addQueryArgFilter('wsh_filter_update_checks');
function wsh_filter_update_checks($queryArgs) {
$settings = get_option('my_plugin_settings');
if ( !empty($settings['license_key']) ) {
$queryArgs['license_key'] = $settings['license_key'];
}
return $queryArgs;
}
Use the Token To Authorize Downloads
Finally, let’s make the update server verify the security token before it lets the user download an update. To do that, you’ll need to create a custom server class (see Extending the server) and override at least the Wpup_UpdateServer::checkAuthorization($request)
method. Here’s what you should do in this method:
- Retrieve the query argument(s) that contains the token by using
$request->param('arg_name')
.
- Verify the token. Again, this part is up to you. You could look it up in a database, use a checksum to validate it, or something else.
- If the token is good, you don’t need to do anything special.
- If the token is invalid, call
$this->exitWithError('Error message')
to output an error and stop script execution.
Below is a simplified version of the script I use to implement secure updates for Admin Menu Editor Pro. It is slightly more advanced than the above outline, but the general idea is the same.
(Again, license management is beyond the scope of this post, so I’ve omitted most of the code that deals with loading and validating licenses. Just treat verifyLicenseExists() and other licensing functions as pseudo-code.)
class SecureUpdateServer extends Wpup_UpdateServer {
protected $licenseServer;
public function __construct($serverUrl, $licenseServer) {
parent::__construct($serverUrl);
$this->licenseServer = $licenseServer;
}
protected function initRequest($query = null, $headers = null) {
$request = parent::initRequest($query, $headers);
//Load the license, if any.
$license = null;
if ( $request->param('license_key') ) {
$result = $this->licenseServer->verifyLicenseExists(
$request->slug,
$request->param('license_key')
);
if ( is_wp_error($result) ) {
//If the license doesn't exist, we'll output an invalid dummy license.
$license = new Wslm_ProductLicense(array(
'status' => $result->get_error_code(),
'error' => array(
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
),
));
} else {
$license = $result;
}
}
$request->license = $license;
return $request;
}
protected function filterMetadata($meta, $request) {
$meta = parent::filterMetadata($meta, $request);
//Include license information in the update metadata. This saves an HTTP request
//or two since the plugin doesn't need to explicitly fetch license details.
$license = $request->license;
if ( $license !== null ) {
$meta['license'] = $this->licenseServer->prepareLicenseForOutput($license);
}
//Only include the download URL if the license is valid.
if ( $license && $license->isValid() ) {
//Append the license key or to the download URL.
$args = array( 'license_key' => $request->param('license_key') );
$meta['download_url'] = self::addQueryArg($args, $meta['download_url']);
} else {
//No license = no download link.
unset($meta['download_url']);
}
return $meta;
}
protected function checkAuthorization($request) {
parent::checkAuthorization($request);
//Prevent download if the user doesn't have a valid license.
$license = $request->license;
if ( $request->action === 'download' && ! ($license && $license->isValid()) ) {
if ( !isset($license) ) {
$message = 'You must provide a license key to download this plugin.';
} else {
$error = $license->get('error');
$message = isset($error) ? $error : 'Sorry, your license is not valid.';
}
$this->exitWithError($message, 403);
}
}
}
If you have any questions, feel free to leave a comment.