Nonces That Are Used Only Once

A while back in this article on wptuts I talked about nonces, and how they work. Normally, nonces are ‘one use only’ – once they are used they are stored, and never accepted again. WordPress, on the other hand, generates a new nonce every 12 hours, each with a life span of 24 hours. They can be used as many times during those 24 hours as needed. And that is, usually, all that is necessary. However instances may occur when you want to create and check nonces that are actually only used once.

Telling the time()

So how can we achieve this? First we want to create a fresh nonce for (essentially) every page load – since if our nonce can only be used once, then you’ll want to be able to do more than one action every 12 hours. I could force WordPress to generate new nonces by shortening the life of the nonce – but this is not really what I want and I might end up with a nonce, valid for one second. Not great.

Instead the idea is to create a standard nonce, whose ‘action’ includes a timestamp. This ensures a fresh nonce is generated every second (say) – which is enough for our purpose (nonces are unique to the user, and should also be unique to the action being taken, and if applicable, the object the action is being applied to). Of course to validate the nonce, we’ll also needed to send the timestamp.

So to create our nonce, we just use wp_create_nonce, but after concatenating it with a timestamp. The returned ‘nonce’ now consists of two parts: the nonce value and timestamp, which we join with hyphen

function sh_create_onetime_nonce($action = -1) {
    $time = time();
    $nonce = wp_create_nonce($time.$action);
    return $nonce . '-' . $time;
}

We can then embed the nonce and timestamp together in the form/url – or however the action is being triggered.

 <form>
    <input type="hidden" name="nonce" value="<?php echo sh_create_onetime_nonce( 'this-action' );?>"/>

    <input type="submit" value="Perform this action">
 </form>

Validating the nonce

Now to verify our nonce. The nonce consists of the original nonce generated by WordPress, combiend witht he timestamp of when it was generated (separated by a hyphen). So first we need to extract the constituent parts.

Concatenating the timestamp part with the action, we use the wp_verify_nonce function agains the nonce part. This checks that the nonce is valid. Of course, it already has a validity lifespan of 24 hours – but in order to reduce the number of used nonces we might have to store, I’ve opted to impose a 1 hour life span as well.

If the nonce is valid, we then check that it hasn’t be used. Used nonces are stored in the database as a serialised array. The array is of the form array( $nonce => $expires_timestamp ) and ordered by timestamp. (I did consider storing them in a separate table, but this is beyond the scope of the article and if we try to keep the number of nonces that might be stored down – say by setting the 1 hour life span, storing them in array shouldn’t present any problems).

If the nonce value hasn’t be used, we proceed to remove any ‘expired’ nonces (ones that would now fail the 1 hour life check) and add this used nonce into the array – and then sorting.

function sh_verify_onetime_nonce( $_nonce, $action = -1) {

    //Extract timestamp and nonce part of $_nonce
    $parts = explode( '-', $_nonce );
    $nonce = $parts[0]; // Original nonce generated by WordPress.
    $generated = $parts[1]; //Time when generated

    $nonce_life = 60*60; //We want these nonces to have a short lifespan
    $expires = (int) $generated + $nonce_life;
    $time = time(); //Current time

    //Verify the nonce part and check that it has not expired
    if( ! wp_verify_nonce( $nonce, $generated.$action ) || $time > $expires )
        return false;

    //Get used nonces
    $used_nonces = get_option('_sh_used_nonces');

    //Nonce already used.
    if( isset( $used_nonces[$nonce] ) ) )
        return false;

    foreach ($used_nonces as $nonce=> $timestamp){
        if( $timestamp > $time )
            break;

        //This nonce has expired, so we don't need to keep it any longer
        unset( $used_nonces[$nonce] );
    }

    //Add nonce to used nonces and sort
    $used_nonces[$nonce] = $expire;
    asort( $used_nonces )
    update_option( '_sh_used_nonces',$used_nonces );
}

And there we have it, nonces that can actually only be used once.