Permit access to downloads, pages or parts of pages for a certain configurable time period and/or for a defined number of access attempts. The access key is tamper-proof.
Requires Textpattern 4.7.0+
Download the plugin from either the official Textpattern plugin repository, the software page, or GitHub, paste the code into the TXP Admin→Plugins pane, install and enable the plugin. The table will be installed automatically unless you use the plugin from the cache directory; in which case, visit the Extensions→Access keys tab and click the Install table button before trying to use the plugin.
To uninstall the plugin, delete from the Admin→Plugins page. The access keys table will be deleted automatically.
Visit the forum thread for more info or to report on the success or otherwise of the plugin.
The plugin allows you to create keys for any URL and you can then protect all or part of that URL by either:
a) wrapping a tag around the parts you want protecting
b) putting a protection tag at the top of the page to protect its entirety
c) flagging the URL as a file_download whereby it’s automatically restricted to only those in possession of the key
An access key is a long URL that looks like this: example.org/some/protected/url/<trigger>/<token>/<timestamps>.<max_accesses>
. Any deviation or alteration of the token results in rejection. The secret key used to protect the resources can be automatically generated by the plugin or supplied by you. A user-configurable salt is applied as well. For best results, let the plugin choose, because then no two secret keys/salts will ever be the same, even if they protect the exact same resource.
You can time-limit the access to the resource if you like — from the moment the key is generated or from some arbitrary point in the future — and/or you can limit the number of accesses (views/downloads) that resource can have. When the resource has expired, it’s gone.
Keys can be generated from the admin side interface (Extensions→Access keys_) or via a public tag. The latter allows you to offer self-key generation, perhaps in response to a mem_self_register request or comconnect form submission, or from an admin-side dashboard.
Generates an access token for a given URL. Configure it using the following attributes:
; url
: The URL of the resource you want to protect. Can be either a fully-qualified URL or relative.
: Default: the current page
; trigger
: This string is added to your url so the plugin knows it is protected content.
: Default: smd_akey
; site_name
: Whether to automatically add the site name (https://example.org) to the URL or not. Choose from:
:: 0: leave the site out
:: 1: add the site URL (but only if its not in the URL already)
: Default: 1
; section_mode
: If you are trying to protect a section landing page (i.e. not an article) then you will likely receive a 404 error when trying to access URLs that entirely use ‘/’ as their separator. By setting section_mode="1"
you tell the plugin to separate the page URL from the access key information with a ‘?’ instead.
: Note this means you cannot use the plugin with the messy mode URL structure, nor can you pass any traditional URL parameters to the page. You can, however, encode such parameters into the extra
area of the key.
: Default: 0
; secret
: A secret key used to unlock the page. Choose any text you wish here or leave blank to have the plugin choose a random number for you.
; strength
: The mechanism used to generate the salt, or the salt itself. In all cases, the salt length will be constrained by the plugin preference value. Choose from the following options:
: SMD_SSL_RAND
: Use PHP’s cryptographically-secure openssl_random_pseudo_bytes()
function, if available (requires PHP 5.3.0+). If it’s not available, it will default to the SMD_MD5 mechanism. It is highly recommended to migrate to this scheme if at all possible.
: SMD_MD5
: Use standard md5()
and uniqid()
functions. These are not cryptographically secure, and were the default for plugin versions prior to v0.20.
: <your own salt>
: Use your own salt value. This option permits you to generate your own cryptographically-secure salt if you wish (e.g. to compute some salt prior to calling this tag and injecting it here). Your salt MUST change every time unless you really, really know what you are doing, and take suitable precautions. See Example 4.
; start
: An English-formatted date stamp from when you want the resource to become available. It is often best to use YYYY-MMM-DD HH:MM:SS
format as it is the least ambiguous.
: Default: empty (i.e. now)
; expires
: An English-formatted date stamp at which you wish the resource to expire, OR an offset beginning with ‘+’, for example expires="+24 hours"
: Default: empty (i.e. no key expiry: get the expiry information from the protected resource itself)
; max
: If you wish to limit the number of times this resource can be requested, set its value here.
: Default: unset (i.e. unlimited)
; extra
: If you want to add any additional information to the URL (after the token) then specify it here. If set, a slash will be added to the URL and then your given content will be appended. It’s up to you to ensure it is suitably encoded. Note that you can add multiple items, separating each with a slash character if you wish. Additional values added using this attribute are directly accessible using the <txp:smd_access_info />
tag
A few words about this tag:
- The clock starts ticking the moment the token is generated (or at the time given in the
start
attribute) - You can pass the returned access link around as often as you like to as many people as you like and it’ll keep working until its expiry/access limit is met
- Each time you create a new token (even for the same page) a new access counter is generated; any previously created tokens that still have available access attempts will continue to function until the limit is reached or they expire
- Direct TXP file downloads can be protected too. Just supply
trigger="file_download"
andurl="/file_download/<id>/<filename>"
. You will then not be permitted to access the example.org/file_download/ link without the access token being present. For this to be effective you should move your/files
directory out of a web-accessible location or employ an .htaccess file that forbids bypassing theexample.org/files/<filename>
URL format. Note that TXP normally allows you to type anything after the/id/
as a filename and still retrieve the file; smd_access_key will not: the filename must match exactly - If the
start
date is mangled in any way, the key will be generated ‘now’ - If the
expires
is mangled in any way, no expiry will be set - Your secret key is salted using a randomly-generated salt, or one of your own choosing. The default length of the salt is 8 characters but you can alter this via a pref
- You may control the expiry time of all of your direct file_download URLs by altering a pref
<txp:smd_access_key
url="/music/next-big-hit" trigger="demo" />
This might generate an access-controlled URL such as the following (newlines just for clarity):
https://example.org/music/next-big-hit/demo/
42c531d13423eecaaab73a2df43a8b7c337a360a/4d9d12a5
Send that link to your friend and she’ll be able to listen to your cool new demo song.
Once you’ve generated a key, it’s time to protect all or part of your chosen URL.
- To protect an entire page, put the
<txp:smd_access_protect>
tag right at the top, above your DOCTYPE - To only protect part of a page, wrap the protected content with the tag
- File downloads are automatically protected if you have generated a key for them
Configure the plugin with the following attributes:
; trigger
: The same trigger you specified in your <txp:smd_access_key />
tag. You may specify a partial trigger if utilising trigger_mode
.
: Default: smd_akey
; trigger_mode
: Usually you’ll want the trigger to match exactly, but you may wish to match some part of the trigger (for a practical example of this, see example 3). Choose from:
:: exact : (default) incoming token’s trigger must exactly match the trigger
attribute
:: begins : incoming token’s trigger must begin with trigger
:: ends : incoming token’s trigger must end with trigger
:: contains : incoming token’s trigger must contain the text given in trigger
: Default: exact
; site_name
: Add the site prefix to the URL when trying to figure out if the resource is protected. This functions identically to the attribute in the <txp:smd_access_key />
tag and, ideally, they should match.
: Default: 1 (yes)
; section_mode
: Works in an identical manner to the attribute in the <txp:smd_access_key />
tag and they should match.
: Default: 0 (no)
; force
: Controls content availability. This is one method of putting the token generation tag and the protected content on the same page. Choose from:
:: 0: permit the resource to be directly accessible
:: 1: the content is unavailable even if you don’t specify the access token in the URL (e.g. if you directly visit /music/next-big-hit
)
: Default: 0
; expires
: The time, in seconds, after which the resource will cease to be available. Set to 0 for unlimited time.
: If you have set an expiry in the key itself, this attribute is ignored when that key is presented.
: Default: 3600 (1 hour)
(if you wish to log IP addresses for each access attempt you can set the pref)
In our music example you’ll probably want to put the following tag at the top of the page:
<txp:smd_access_protect expires="86400" trigger="demo" force="1" />
So nobody can visit that page unless they have been given a valid access token URL. The song will be available for listening for just one day from the moment the token is generated.
Alternatively, you might allow people to see the surrounding page and just wrap the mp3 itself with the tag:
<txp:smd_access_protect expires="86400" trigger="demo">
<object type="/audio/mpeg" data="/music/next-big-hit.mp3" />
</txp:smd_access_protect>
Note that you can protect as many parts of the same page as you like, differing only in the trigger
. This allows you to unlock various parts of your pages to different people and set individual expiry times for each portion of the page, or set expiry times in the access tokens themselves.
Another option is is to elect to create the access key directly for the file_download itself. If you’re feeling paranoid you could do both :-)
Conditional tag that triggers the contained content if access was denied for some reason. Supports <txp:else />
. Attributes:
; type
: Comma-delimited list of one or more of the following access types. The corresponding HTTP error status code thrown is given in parentheses:
:: smd_akey_err_bad_token (403) : if the access token in the URL is too long/short
:: smd_akey_err_expired (410) : if the resource has passed its expiry
:: smd_akey_err_forbidden (401) : triggered if the force
attribute is set and direct access has been requested
:: smd_akey_err_invalid_token (403) : the access key in the URL doesn’t match the one used to protect the resource
:: smd_akey_err_limit (410) : the number of permitted views (specified via the max
attribute) has been exceeded
:: smd_akey_err_missing_timestamp (403) : the timestamp portion of the access key is missing from the URL
:: smd_akey_err_unavailable (410) : the resource isn’t yet available (the start time hasn’t been reached)
:: smd_akey_err_unauthorized (401) : the resource isn’t one that has been protected by this access key
; code
: Comma-delimited list of one or more of the access codes given in the above list
Without any attributes, the tag triggers the contained content on any error. If you specify many @type@s then any of the listed types that match will return true. The same goes for @code@s. If you use the two in tandem then the error must match one of the types AND one of the codes to return true.
Display an access error message/code. Typically used inside <txp:smd_if_access_error>
. Attributes:
; item
: Which error information to display. Comma-separate the values if you wish to use both. Choose from:
:: message
: the error string
:: code
: the status code
: Default: message
; message
: Display this message instead of the plugin default message.
; wraptag
: HTML tag (without angle brackets) to wrap around the output.
; class
: CSS class name to apply to the wraptag.
; html_id
: HTML ID to apply to the wraptag.
; break
: HTML tag (without angle brackets) to wrap around each item.
; breakclass
: CSS class name to apply to each break tag
Display access information from the current protected page. Attributes:
; item
: One or more of the following items (comma-delimited):
:: page : the URL that matches the current access key
:: trigger : the trigger for the current key, as given in the URL
:: maximum : the maximum number of access attempts allowed for this key
:: accesses : the current number of access attempts that have been made for this key
:: now : the raw UNIX timestamp of the time the access attempt was made
:: issued : the raw UNIX timestamp of when the current key was created
:: expires : the raw UNIX timestamp of when the current key expires
:: hextime : the UNIX timestamp of when the current key was created, encoded as a hex string (as used in the URL)
:: ip : the IP address of the remote address making the request (if available)
:: extra : the full string as given in the extra
attribute
:: extra_N (where N is an integer beginning at 1) : if you have specified many values in your extra
, each separated with a slash, this displays the given individual item
: Default: page
; escape
: Whether to escape HTML entities such as <
, >
and &
in the item. Set escape=""
to turn this off.
: Default: html
; format
: If displaying a time-related item (now, issued, or expires) you can format it using the strftime() formatting codes.
: Default: %Y-%m-%d %H:%M:%S
; wraptag
: HTML tag (without angle brackets) to wrap around the output.
; class :
CSS class name to apply to the wraptag.
; html_id
: HTML ID to apply to the wraptag.
; break
: HTML tag (without angle brackets) to wrap around each item.
; breakclass
: CSS class name to apply to each break tag.
Under Extensions→Access keys you will see a list of all keys that have been generated. You can sort the list (ascending or descending) by clicking the headings or you can filter them using the select list and text box at the top. The usual multi-edit tools/checkboxes are available to allow you to batch-delete keys.
If you want to manually create an access key, click New key and fill in as many of the text boxes as you wish. The only one that is mandatory is the Page box. Your access key will be generated and displayed in the message area for you to copy and paste.
A few things to note about the list:
- If you have elected to log IP addresses (see prefs) the IPs of all key-based access attempts are logged. Further, if you have TXP visitor logging enabled, the addresses in the ‘IP’ column are hyperlinked to the Visitor Logs to show you all accesses by the clicked IP. Although the plugin can potentially hold over 1600 IP addresses the interface might crumble before you reach this limit so it’s best to try and limit the number of accesses per key to keep the number of attempts low and make the interface manageable
- The Page is hyperlinked to the protected URL but it does not attempt to access it with the key. It is just a convenience for you to check that the page is protected. If you want to access the page via its key you can do so from the Visitor Logs, assuming someone has tried at least once to access the resource — you can just click the relevant visitor log Page entry which contains the full access key URL
- The number of accesses may exceed the maximum permitted. This is because attempts are logged all the time the resource is available (i.e. has not expired). Rest assured if the maximum access limit has been reached, visitors with valid keys will NOT see the protected content, even though the counter will continue to log requests. You can use this information to check the TXP Visitor Logs for IP addresses that try to access a resource after the limit has been reached. As soon as the resource expires, access counting ceases
- The trigger for file downloads is empty. This is a side-effect of the way the plugin operates but it has a useful bonus: you can leave the trigger blank if your Page URL contains
/file_download
(because the trigger itself is the presence offile_download
in the URL)
The plugin exposes some global preference settings that govern its operation:
; File download expiry time
: Time, in seconds, after which your file downloads will expire, following generation of an access key.
: Default: 3600 (1 hour)
; Salt length
: Number of characters to use as a salt in your secret key. The default is usually fine but if you want to alter this for greater/reduced security then do so. IMPORTANT: if you change this value, all your existing access keys become instantly invalid.
: Default: 8
; Access key length
: Number of characters to use as a token in your final key. The default delivers fairly long URLs so if you prefer to shorten this for ease of use, then do so. IMPORTANT notes: a) if you change this value, all your existing access keys become instantly invalid. b) the shorter the value, the easier it will be to crack.
: Default: 32
; Log IP addresses
: Whether you wish the plugin to log the IP address of each visitor who tries to access a protected resource
: Default: no
<txp:smd_access_protect
trigger="leaflets" force="1" expires="86400">
<p>Your leaflet contents goes here</p>
<txp:else />
<txp:smd_if_access_error
type="smd_akey_err_forbidden">
<p>Before you can download this item, you'll need
an access key.
<a href="<txp:smd_access_key max="4"
trigger="leaflets" />">Here's one</a></p>
<txp:else />
<txp:smd_access_error item="code, message"
break="br" />
</txp:smd_if_access_error>
</txp:smd_access_protect>
<txp:smd_access_protect
trigger="leaflet, book" expires="86400">
<p>Your <txp:smd_access_info item="trigger" />
contents goes here</p>
<txp:else />
<txp:smd_if_access_error>
<txp:smd_access_error item="code, message"
break="br" />
</txp:smd_if_access_error>
</txp:smd_access_protect>
You can use smd_if, or assign the output of <txp:smd_access_info>
to a <txp:variable>
and use <txp:if_variable>
, to perform conditional display of access key information.
In your registration e-mail form, you could put something like this:
Thank you for registering with <txp:site_name />.
To activate your account, please visit the following
link within the next 24 hours:
<txp:smd_access_key url="/account/activate/" max="1"
trigger='user_id.<txp:author title="0" />' />
Once the visitor has signed up they are e-mailed a unique access token to the /account/activate
page. You can protect it like this:
<txp:smd_access_protect trigger="user_id."
trigger_mode="begins" expires="86400" force="1">
<txp:hide> Activation actions could go here </txp:hide>
<p>Congratulations, your account is activated.</p>
<txp:else />
<txp:smd_access_error />
</txp:smd_access_protect>
If necessary, you could employ str_replace()
(or <txp:rah_replace />
) to remove the user_id.
and be left with the user name of the registered user name, suitable for display. Alternatively — and perhaps easier — you could pass the author as an extra
parameter when the key is generated:
<txp:smd_access_key url="/account/activate/" max="1"
trigger="user_id" extra='<txp:author title="0" />' />
This allows you to use the default trigger_mode
and has the benefit that you can directly access the additional piece of information using <txp:smd_access_info item="extra_1" />
.
The extra
attribute is even more powerful if you pack more information into it: extra='<txp:author title="0" />/<txp:section />'
. You can retrieve each piece individually:
<txp:smd_access_info item="extra" />
=>sdawson/account
<txp:smd_access_info item="extra_1" />
=>sdawson
<txp:smd_access_info item="extra_2" />
=>account
Note that you can only extract each piece individually if you use a slash as a separator in your extra
attribute.
You will often use smd_access_key in response to a user action, such as paying for access to a resource. In this situation, you would normally email an access key upon successful payment. But sometimes you may wish to return control to your website and show the access key on-screen so there is no permanent record of it.
The trouble with this approach is that if the person refreshes the screen, they will get a different access key to the same resource, thereby effectively having the ability to generate unlimited keys. This is undesirable.
From v0.20 onwards, you may now specify your own salt, which can be used to create a static key. But it comes with some important security considerations:
- Your salt is included in the access key itself and is thus plainly visible. This is not normally a security concern because the secret key protects the token, but to make a fixed key you need a static secret key too.
- A static secret key must still be unique to the transaction. Never share a secret key between transactions or users or connections.
- You will also need to use a static
start
date and time. In this situation, a good value to use would be the order time.
To understand this fully, it’s important to grasp how an access token is generated. It consists of three parts:
- A secret key (random string of hex digits), which is usually generated at runtime and is unique.
- A salt (random string of hex digits), again, uniquely generated at runtime.
- A timestamp (YYYY-MM-DD HH:mm:ss) which defaults to ‘now’, i.e. the very instant the
<txp:smd_access_key />
tag is rendered.
Assuming the URL of the resource doesn’t change, if all three of the above components are identical on each page request, the same key will be generated. As this is our goal, here, let’s design a system to do that:
- Secret key: a hash of the buyer’s username and the transaction ID, plus a fixed secret string of your own choosing.
- Salt: a hash of the buyer’s email address. The length of this must be greater than or equal to the value in the Salt length preference, otherwise the token will not match.
- Timestamp: the time of the transaction.
So, for any given transaction, the above three items do not vary and will therefore generate the same key. But a transaction that occurs from someone else, even if it’s at the exact same moment, will produce an entirely different, yet invariant, key.
So here’s some not-very-super-secure example code, assuming that you have the order information populated in some PHP variables. You probably wouldn’t use md5()
here, but something altogether more cryptographically secure:
<txp:variable name="secretKey"><txp:php>echo md5($username . $txnId . "I'mABadSecretKey");</txp:php></txp:variable> <txp:variable name="salt"><txp:php>echo md5($email);</txp:php></txp:variable> <txp:variable name="timestamp"><txp:php>echo $orderTime;</txp:php></txp:variable>
<txp:smd_access_key url='<txp:site_url /><txp:section />' trigger="my-trigger" secret='<txp:variable name="secretKey" />' strength='<txp:variable name="salt" />' start='<txp:variable name="timestamp" />' />
Ta da!
Written by Stef Dawson.