diff --git a/README.md b/README.md index 5acda83..9b47a85 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ It needs to be running PHP 5.3 or higher. It requires the [deliciousbrains/wp-migration](https://github.com/deliciousbrains/wp-migrations) package and so the site will need to be set up to run `wp dbi migrate` as a last stage build step in your deployment process. +You should also run `wp dbi migrate` after updating the package to make sure you have up to date database tables. + It automatically purges expired keys from the database daily, and there are WP-CLI commands to: 1. Manually purge expired keys @@ -31,16 +33,20 @@ There are two parameters you can pass when bootstrapping the package: To generate a URL that will automatically login a user and land them at a specific URL use this function: -`dbi_get_auto_login_url( $destination_url, $user_id, $query_parms );` +`dbi_get_auto_login_url( $destination_url, $user_id, [$query_params], [$expiry], [$one_time] );` The URL will expire in 120 days. However, you can pass the number of seconds the URL will be valid for as the fourth argument, e.g valid for 1 day: -`dbi_get_auto_login_url( $destination_url, $user_id, $query_parms, 86400 );` +`dbi_get_auto_login_url( $destination_url, $user_id, $query_params, 86400 );` You can also specify your own global default for expiry when bootstrapping the package as explained in the "Installation" section above. Use: `\DeliciousBrains\WPAutoLogin\AutoLogin::instance( 'dbi', );` +There is also an option to generate links that can only be used once: + +`dbi_get_auto_login_url( $destination_url, $user_id, $query_params, null, true );` + ## WP-CLI There are two WP-CLI commands. @@ -74,3 +80,7 @@ Example: `wp dbi auto_login_url 12345 https://example.com/dashboard --expiry=21600` Will generate a link that logs in the user with ID 12345 and takes them to https://example.com/dashboard. The link will be valid for 6 hours. + +You can add `--one-time` to generate a single-use link: + +`wp dbi auto_login_url 12345 https://example.com/dashboard --one-time` diff --git a/functions.php b/functions.php index 6265c23..27a98ce 100644 --- a/functions.php +++ b/functions.php @@ -5,9 +5,9 @@ } if ( ! function_exists( 'dbi_get_auto_login_url' ) ) { - function dbi_get_auto_login_url( $url, $user_id, $args = array(), $expires_in = null ) { + function dbi_get_auto_login_url( $url, $user_id, $args = array(), $expires_in = null, $one_time = false ) { $autologin = \DeliciousBrains\WPAutoLogin\AutoLogin::instance(); - return $autologin->create_url( $url, $user_id, $args, $expires_in ); + return $autologin->create_url( $url, $user_id, $args, $expires_in, $one_time ); } -} \ No newline at end of file +} diff --git a/migrations/2021_09_05_add_single_use_to_keys_table.php b/migrations/2021_09_05_add_single_use_to_keys_table.php new file mode 100644 index 0000000..cfe6975 --- /dev/null +++ b/migrations/2021_09_05_add_single_use_to_keys_table.php @@ -0,0 +1,18 @@ +query( " + ALTER TABLE `{$wpdb->prefix}dbrns_auto_login_keys` + ADD COLUMN `one_time` tinyint NOT NULL DEFAULT 0; + " ); + } +} diff --git a/src/AutoLogin.php b/src/AutoLogin.php index 09a36de..ee4c199 100644 --- a/src/AutoLogin.php +++ b/src/AutoLogin.php @@ -34,7 +34,7 @@ class AutoLogin { * @return AutoLogin Instance */ public static function instance( $command_name = 'dbi', $expires = 10368000 ) { - if ( ! isset( self::$instance ) && ! ( self::$instance instanceof AutoLogin ) ) { + if ( ! isset( self::$instance ) || ! ( self::$instance instanceof AutoLogin ) ) { self::$instance = new AutoLogin(); self::$instance->init( $command_name, $expires ); } @@ -121,7 +121,9 @@ public function handle_auto_login() { return; } - $user_id_for_key = $this->get_user_id_for_key( $login_key ); + $key = AutoLoginKey::get_by_key( $login_key ); + + $user_id_for_key = $this->get_user_id_for_key( $key ); if ( $user_id_for_key === false || $user_id_for_key != $user->ID ) { do_action( 'wp_login_failed', $user->user_login ); @@ -132,6 +134,8 @@ public function handle_auto_login() { wp_set_auth_cookie( $user->ID ); do_action( 'wp_login', $user->user_login, $user ); + $key->maybe_delete_one_time_key(); + $redirect = remove_query_arg( [ 'login_key', 'user_id' ] ); wp_redirect( $redirect ); exit; @@ -142,9 +146,7 @@ public function handle_auto_login() { * * @return bool|int */ - public function get_user_id_for_key( $key_to_find ) { - $key = AutoLoginKey::get_by_key( $key_to_find ); - + public function get_user_id_for_key( $key ) { if ( ! $key ) { return false; } @@ -157,12 +159,13 @@ public function get_user_id_for_key( $key_to_find ) { } /** - * @param int $user_id - * @param null|int $expires_in Seconds + * @param int $user_id + * @param null|int $expires_in Seconds + * @param null|boolean $one_time * * @return bool|string */ - public function create_key( $user_id, $expires_in = null ) { + public function create_key( $user_id, $expires_in = null, $one_time = false ) { do { $key = wp_generate_password( 40, false ); $already_exists = AutoLoginKey::get_by_key( $key ); @@ -171,6 +174,7 @@ public function create_key( $user_id, $expires_in = null ) { $loginkey = new AutoLoginKey(); $loginkey->login_key = $key; $loginkey->user_id = $user_id; + $loginkey->one_time = $one_time; if ( $expires_in ) { $loginkey->expires = gmdate( 'Y-m-d H:i:s', time() + $expires_in ); } else { @@ -197,15 +201,16 @@ public function remove_expired_keys() { } /** - * @param string $url - * @param int $user_id - * @param array $args - * @param null|int $expires_in Seconds + * @param string $url + * @param int $user_id + * @param array $args + * @param null|int $expires_in Seconds + * @param null|boolean $one_time * * @return string */ - public function create_url( $url, $user_id, $args = [], $expires_in = null ) { - $login_key = $this->create_key( $user_id, $expires_in ); + public function create_url( $url, $user_id, $args = [], $expires_in = null, $one_time = false ) { + $login_key = $this->create_key( $user_id, $expires_in, $one_time ); $args = array_merge( [ 'login_key' => $login_key, diff --git a/src/CLI/Command.php b/src/CLI/Command.php index 40fdf3a..e0d4924 100644 --- a/src/CLI/Command.php +++ b/src/CLI/Command.php @@ -23,6 +23,9 @@ class Command extends \WP_CLI_Command { * --- * default: 172800 * --- + + * [--one-time] + * : Make the login key work only once * * @param array $args * @param array $assoc_args @@ -41,13 +44,16 @@ public function auto_login_url( $args, $assoc_args ) { return \WP_CLI::warning( 'User not found' ); } - // Validate expiry + // Validate one-time option. + $one_time = isset( $assoc_args['one-time'] ) ? (bool) $assoc_args['one-time'] : false; + + // Validate expiry. Note that WP-CLI always provides a default so we don't need an isset check. if ( ! is_numeric( $assoc_args['expiry'] ) ) { \WP_CLI::error( 'Please specify a numeric value for the expiry.' ); } - $url = empty( $args[1] ) ? home_url() : $args[1]; - $key_url = AutoLogin::instance()->create_url( $url, $user->ID, array(), $assoc_args['expiry'] ); + $url = empty( $args[1] ) ? home_url() : $args[1]; + $key_url = AutoLogin::instance()->create_url( $url, $user->ID, array(), $assoc_args['expiry'], $one_time ); return \WP_CLI::success( 'Auto-login URL generated: ' . $key_url ); } diff --git a/src/Model/AutoLoginKey.php b/src/Model/AutoLoginKey.php index 6601470..4cbfd2b 100644 --- a/src/Model/AutoLoginKey.php +++ b/src/Model/AutoLoginKey.php @@ -44,6 +44,13 @@ class AutoLoginKey { */ public $expires; + /** + * Flag to indicate that key can only be used once + * + * @var boolean + */ + public $one_time; + /** * Constructor * @@ -56,11 +63,19 @@ public function __construct( $attributes = array() ) { if ( ! isset( $attributes['created'] ) ) { $this->created = gmdate( 'Y-m-d H:i:s', time() ); } + + if ( ! isset( $attributes['one_time'] ) ) { + $this->one_time = false; + } else { + $this->one_time = (bool) $attributes['one_time']; + } } /** * Fetches a key object for the specified key. * + * If the key is one-time then it will be deleted here too. + * * @param string $key The key to fetch a record for. * @return AutoLoginKey|null The key object to return, or null if not found. */ @@ -81,6 +96,21 @@ public static function get_by_key( $key ) { return new self( $row ); } + /** + * Check if the key is one-time and to delete it from the database if it is. + * + * @return int|bool The number of rows deleted, or false if an error occurred. + * Note that both false and 0 can be returned so be careful with + * comparisons. + */ + public function maybe_delete_one_time_key() { + global $wpdb; + + $table = self::$table; + + return $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->prefix{$table} WHERE login_key = %s AND one_time = 1", $this->login_key ) ); + } + /** * Static method to delete legacy keys from the database. * These keys will have an expiry of '0000-00-00 00:00:00', and will need to be @@ -130,6 +160,7 @@ public function save() { 'user_id' => $this->user_id, 'expires' => $this->expires, 'created' => $this->created, + 'one_time' => (int) $this->one_time, ); return $wpdb->insert( $wpdb->prefix . self::$table, $data );