Plugin Updates: Securing Download Links

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:

  1. 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.
  2. Whenever someone installs your plugin on their site, ask them to enter the key/log in/whatever.
  3. Modify the plugin to append the token to each update request.
  4. 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):

Admin Menu Editor Pro entry on the "Plugins" page

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:

  1. Retrieve the query argument(s) that contains the token by using $request->param('arg_name').
  2. 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.
  3. If the token is good, you don’t need to do anything special.
  4. 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.

Related posts :

38 Responses to “Plugin Updates: Securing Download Links”

  1. Robert says:

    finally got it – no need to answer my last comment 😉

  2. Janek says:

    I’ve been using your libraries for theme and plugin auto updates. Both of them are simple to implement and work great. I’m looking forward to implementing the WP Update Server.

  3. Erwin says:

    Hi, is there a tutorial how to do this? My PHP knowledge is not good enough to fix this. Or can you help me to fix this? I have no idea how to start. I use your Plugin Update Checker with the WP Update Server and that work awesome.

  4. Jānis Elsts says:

    There’s no other tutorial other than what you see on this page. Everyone has different ideas about how they want to protect their download links – username/password, subscriptions, license keys, or something else – so it’s not really possible to write a general-purpose tutorial. I’m afraid you’ll have to figure out most of it yourself 🙂

  5. Erwin says:

    I like the way you use with a license key, but thnx I try to figure it out.

  6. joseph says:

    your libs are very straightforward. thanks

  7. Dan says:

    Would you happen to know of any way to post some parameters when WordPress goes to the download url for the zip?

  8. Cornelis says:

    Just wanted to thank you for your great, better, best contributions to Github with the Plugin Update Checker and the Update Server.

    Thank you, you’re a life safer 😉

  9. Jānis Elsts says:

    Thanks, I’m glad to hear you like it 🙂

  10. Thanks for the great update checker and the surrounding infrastructure. The only thing that was missing for us was a nice GUI to enter the download credentials.

    We have now extended your download checker to let the user enter the download credentials directly on the plugins admin page. The class (extends your download checker class) is implemented in a generic way (i.e. not tied to the specific download credentials our download system uses):
    http://open-tools.net/documentation/tutorial-automatic-updates.html#wordpress

    I put this extension under the MIT license, too. In my opinion, our code can be directly included in your plugin checker to provide a possible GUI component, too. If the user does not register any credentials, our extended class should behave just like your base update checker.

    Best regards,
    Reinhold

  11. Jānis Elsts says:

    @Matt: I’ve seen your GitHub issue, but unfortunately I’m a bit sick and don’t have the time for debugging. I’ll look into it when I can.

  12. I had a problem where the server wasn’t returning proper json. Fixed by changed line 9 of the SecureUpdateServer class from:

    protected function initRequest($query) {

    to

    protected function initRequest($query = NULL, $headers = NULL) {

    hopefully this saves someone else the time it took me to figure out.

  13. Jānis Elsts says:

    Thanks for the report, I’ve fixed the code sample.

    (This post was originally written for an older version of the update server where the initRequest method signature was different. The $headers parameter was added later.)

  14. Hello, I have installed a licensing system (Software License Manager – https://wordpress.org/plugins/software-license-manager/) please be done instructions on how to import the updates? Your use WP Update Server

    Thank you

  15. Jānis Elsts says:

    Sorry, that’s something you might have to figure out yourself. Right now I don’t have the time to research and produce tutorials on how to integrate with third-party licensing systems.

  16. Matt says:

    I have a small question, How do i add the banners to the plugins so its shows up on the users website (View details)

  17. Matt says:

    Never mind about banners. I added this to my filterMetadata function.

    $low_banner = ‘http://’ . $server_url . ‘banners/’ . $slug .’-772-250.png’;
    $high_banner = ‘http://’. $server_url . ‘banners/’ . $slug .’-1544-500.png’;
    $banner_array = array(‘low’ => $low_banner,’high’ => $high_banner);
    $meta[‘banners’] = $banner_array;

    This could help others, Happy coding 🙂

  18. Mike says:

    Hi, thanks for you work, it’s great!
    In your example…

    /* … Code that initializes the update checker … */

    //Add the license key to query arguments.
    $updateChecker->addQueryArgFilter(‘wsh_filter_update_checks’);
    public 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;
    }

    … why the function wsh_filter_update_checks() is a class method?
    I’m using the update-checker in my plugin, and I’m initializing it like that…

    require ‘path/to/plugin-update-checker/plugin-update-checker.php’;
    $myUpdateChecker = Puc_v4_Factory::buildUpdateChecker(
    ‘http://example.com/path/to/details.json’,
    __FILE__,
    ‘unique-plugin-or-theme-slug’
    );

    … so, using the filter above would be…

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

    … am I right?
    Anyway, it is not working at the moment. The ‘license_key’ is not passed as argument and I can’t see it in my MyCustomServer class.
    Do am I missing something? Any suggestion?
    Thanks in advance.

Leave a Reply