Plugin Updates: Securing Download Links

March 19th, 2013

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');
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;
}

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) {
		$request = parent::initRequest($query);

		//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.


WordPress Update Server

March 12th, 2013

It’s been a long time coming, but I’ve finally released an external update API server for WordPress plugins and themes. Check it out on GitHub. This is the server component to my plugin update checker and theme update checker client libraries.

Features

  • Provide updates for private or commercial plugins and themes.
    From the users’ perspective, the updates work just like they do with plugins and themes listed in the official WordPress.org directory.
  • Easy to set up.
    Just upload the script to your site and drop a plugin or theme Zip in the “packages” subdirectory.
  • Easy to integrate with existing plugins and themes.
    All it takes is about 5 lines of code.
  • Minimal server requirements.
    The server component requires PHP 5.3+ and the Zip extension. The update checker component only requires PHP 5.2 – same as the current version of WordPress.
  • Extensible.
    Want to secure your upgrade download links? Or use a custom logger or cache? Maybe you ‘d like to load the change log and other update meta from the database instead of parsing it from a Zip file? Create your own, customized update server by extending the Wpup_UpdateServer class.

What’s Next?

See the GitHub repository for download links and usage instructions. The documentation still needs work, but it should be good enough to get you up and running.


Plugin Compatibility Reporter

December 4th, 2012

Plugin Compatibility Reporter is a WordPress plugin that enables you report if the plugins that you’re using are working properly. It integrates with the compatibility reporting features of the official WordPress.org plugin directory, and lets you mark plugins as “working” or “broken” from within your WordPress dashboard.

It also tracks how long each plugin has been active on your site and automatically reports plugins that have been active for a while (1 week by default) as compatible – unless, of course, you’ve already marked them as broken. This way you won’t need to waste time on reporting that some plugins work fine.

See also: Why use this plugin?

Screenshots

Plugin settings page

You can use these “works” and “broken” links to report if a plugin is working properly with your WP version

Download

Requires WordPress 3.4 or later.

Installation

  1. Go to Plugins -> Add New -> Upload and upload the plugin archive to your site.
  2. Activate the plugin via the “Plugins” page.
  3. Go to Plugins -> Compatibility Reporter and enter your WordPress.org credentials in the appropriate fields. If you don’t have a WordPress.org account yet, you can get one here.
  4. (Optional) Change how long a plugin needs to be active before it gets marked as working. The default is 1 week.
  5. The plugin will now download your existing compatibility votes. This happens in the background and can take up to 10 minutes.
  6. You should see “Works” and “Broken” links on your “Plugins” page. Click them to report your plugins as compatible or incompatible with your WordPress version. Your current vote (if any) will be displayed in bold.

Why Use It?

The main goal of this plugin is to make the compatibility reports that WordPress.org displays for each plugin more useful. Right now, most plugins only have a handful of votes – not enough to reliably determine if they’re compatible with this or that WordPress version.

Exhibit A

Even plugins that have been downloaded millions of times usually have only a couple dozen of compatibility reports for the current version, at best. Why is that, and how does this plugin help the situation?

1. Reporting plugin compatibility is inconvenient.

To be able to vote, you first have to sign up for a WordPress.org account. That’s easy enough and you only need to do it once. Then, when you actually want to report a plugin as working or broken, you need to:

  1. Go to WordPress.org.
  2. Remember your username and password.
  3. Find the plugin you want to vote on. There might be a “Visit plugin homepage” link on the “Plugins” page, but that usually leads to the author’s site, not the WordPress.org listing.
  4. Find your plugin version in the drop-down list.
  5. Find your WordPress version in the second list.
  6. Click “Works” or “Broken”.

Multiply this by the number of plugins you have installed and the number of plugin and WordPress updates, and you’ll see why most people don’t bother with it. This plugin simplifies the process down to clicking a “Works” or “Broken” link on your “Plugins” page. You only have to enter your WordPress.org account details once: when setting up the plugin.

2.There’s little incentive to mark plugins as working.

Of those users who do send compatibility reports, many do it only when a plugin doesn’t work. Even if you deliberately try to avoid this and always report compatible plugins as working, it can get tiresome quickly since new plugin versions are released all the time.

One side-effect of this is that compatibility figures tend to be unfairly biased towards “broken”. For example, take Jetpack which currently has 29 “works” and 8 “broken” votes. I seriously doubt that a plugin that was made by the same folk who run WordPress.com is broken on 8/37 = 22% of sites.

Plugin Compatibility Reporter solves this by automatically marking plugins that have been active for a long time as working. This is based on the assumption that the user wouldn’t leave a plugin active if it was incompatible with their site. Of course, you can always change your vote to “broken” if it turns out that the plugin had a subtle bug that you didn’t notice at first.  This plugin will not overwrite your existing reports.

Benefits

  • Users get a better idea of how well any given plugin works with the latest version of WordPress.
  • Developers get a more accurate impression of what fraction of users are having problems with their plugin.
  • Less time is wasted on reporting plugins that work properly.

How to Pre-Select a Category for a New Post

November 20th, 2012

Lets say you have a category called “X”, and you want to create a link which when clicked will take the user to the “Add New Post” screen with the “X” category already selected. This could be used to simplify posting for non-technical users, or even as a way […] Continue Reading…


Magic Quotes in WordPress, and How To Get Rid of Them

November 13th, 2012

One thing that many developers are not aware of is that WordPress automatically adds magic quotes to request variables. This means that all quotes, backslashes and null-byte characters will be escaped with a backslash.

Even if you disable magic quotes in php.ini, WordPress will apply them to $_GET, $_POST, $_COOKIE […] Continue Reading…