wp_generate_password( TOKEN_LENGTH, false ),
// todo Ideally should encrypt at rest, see https://core.trac.wordpress.org/ticket/24783.
'expiration' => time() + ( 2 * HOUR_IN_SECONDS ),
'use_once' => $use_once,
);
/*
* Tying the token to a specific pledge is important for security, otherwise companies could get a valid token
* for their pledge, and use it to edit other company's pledges.
*
* Similarly, tying it to specific actions is also important, to protect against CSRF attacks.
*
* This function intentionally requires the caller to pass in a pledge ID and action, so that it can guarantee
* that each token will be unique across pledges and actions.
*/
update_post_meta( $pledge_id, TOKEN_PREFIX . $action, $auth_token );
$auth_url = add_query_arg(
array(
'action' => $action,
'pledge_id' => $pledge_id,
'auth_token' => $auth_token['value'],
),
get_permalink( $action_page_id )
);
// todo include a "this lnk will expire in 10 hours and after its used once" message too?
// probably, but what's the best way to do that DRYly?
return $auth_url;
}
/**
* Verify whether or not a given authentication token is valid.
*
* These tokens are more secure than WordPress' imitation nonces because they cannot be reused[1], and expire
* in a shorter timeframe. Like WP nonces, though, they must be tied to a specific action and post object in order
* to prevent misuse.
*
* [1] In some cases, tokens can be reused, when that is explicitly required for their flow. For an example, see
* the documentation in `get_authentication_url()`.
*
* @param int $pledge_id
* @param string $action
* @param string $unverified_token
*
* @return bool
*/
function is_valid_authentication_token( $pledge_id, $action, $unverified_token ) {
$verified = false;
$valid_token = get_post_meta( $pledge_id, TOKEN_PREFIX . $action, true );
/*
* Later on we'll compare the value to user input, and the user could input null/false/etc, so let's guarantee
* that the thing we're comparing against is really what we expect it to be.
*/
if ( ! is_array( $valid_token ) || ! array_key_exists( 'value', $valid_token ) || ! array_key_exists( 'expiration', $valid_token ) ) {
return false;
}
if ( ! is_string( $valid_token['value'] ) || TOKEN_LENGTH !== strlen( $valid_token['value'] ) ) {
return false;
}
if ( ! is_string( $unverified_token ) || TOKEN_LENGTH !== strlen( $unverified_token ) ) {
return false;
}
if ( $valid_token && $valid_token['expiration'] > time() && hash_equals( $valid_token['value'], $unverified_token ) ) {
$verified = true;
// Tokens should not be reusable -- to increase security -- unless explicitly required to fulfill their purpose.
if ( false !== $valid_token['use_once'] ) {
delete_post_meta( $pledge_id, TOKEN_PREFIX . $action );
}
}
return $verified;
}
/**
* Checks user capabilties or auth token to see if this user can edit the given pledge.
*
* @param int $requested_pledge_id The pledge to edit.
* @param string $auth_token The supplied auth token to check.
*
* @return true|WP_Error
*/
function can_manage_pledge( $requested_pledge_id, $auth_token = '' ) {
// A valid token superceeds other auth methods.
if ( true === is_valid_authentication_token( $requested_pledge_id, 'manage_pledge', $auth_token ) ) {
return true;
} else if ( is_user_logged_in() ) {
if ( current_user_can( 'manage_options' ) ) {
return true;
}
return new \WP_Error(
'invalid_token',
sprintf(
__( 'You don\'t have permissions to edit this page. Request an edit link.', 'wporg-5ftf' ),
get_permalink( $requested_pledge_id )
)
);
}
return new \WP_Error(
'invalid_token',
sprintf(
__( 'Your link has expired, please obtain a new one.', 'wporg-5ftf' ),
get_permalink( $requested_pledge_id )
)
);
}