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 capabilities 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 supersedes 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 ) ) ); }