From adcc72446ed28e77ae794964df693942ac0662e1 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Tue, 5 Oct 2021 12:27:41 +0100 Subject: [PATCH 1/9] Add one-time option for single-use keys. --- ...021_09_05_add_single_use_to_keys_table.php | 18 ++++++++++ src/AutoLogin.php | 21 ++++++----- src/CLI/Command.php | 5 ++- src/Model/AutoLoginKey.php | 36 +++++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 migrations/2021_09_05_add_single_use_to_keys_table.php 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 95f2516..fe91e65 100644 --- a/src/AutoLogin.php +++ b/src/AutoLogin.php @@ -157,12 +157,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 +172,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 +199,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..c32f9b6 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 @@ -47,7 +50,7 @@ public function auto_login_url( $args, $assoc_args ) { } $url = empty( $args[1] ) ? home_url() : $args[1]; - $key_url = AutoLogin::instance()->create_url( $url, $user->ID, array(), $assoc_args['expiry'] ); + $key_url = AutoLogin::instance()->create_url( $url, $user->ID, array(), $assoc_args['expiry'], $assoc_args['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..32251e9 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,17 @@ 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; + } } /** * 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. */ @@ -78,9 +91,31 @@ public static function get_by_key( $key ) { return null; } + if ( (bool) $row['one_time'] ) { + self::maybe_delete_one_time_key( $key ); + } + return new self( $row ); } + /** + * Static method to check if a key is one-time and to delete it from the + * database if it is. + * + * @param string $key + * + * @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 static function maybe_delete_one_time_key( $key ) { + global $wpdb; + + $table = self::$table; + + return $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->prefix{$table} WHERE login_key = %s AND one_time = 1", $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 +165,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 ); From 4c5053c4c004b584f491014dea1e96e8e05f1b7c Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Tue, 5 Oct 2021 13:45:46 +0100 Subject: [PATCH 2/9] Add one_time parameter to the dbi_get_auto_login_url() function --- functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 +} From 2171949b4f54800a1d7527b6b216e0ab6b30718e Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Tue, 5 Oct 2021 13:47:01 +0100 Subject: [PATCH 3/9] Update README --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5acda83..c1470ac 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_parms, 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` From a5e3a8f5ebbbb16fc5cb6862e643c2710da6b911 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Tue, 5 Oct 2021 13:47:54 +0100 Subject: [PATCH 4/9] Typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c1470ac..9b47a85 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ You can also specify your own global default for expiry when bootstrapping the p There is also an option to generate links that can only be used once: -`dbi_get_auto_login_url( $destination_url, $user_id, $query_parms, null, true );` +`dbi_get_auto_login_url( $destination_url, $user_id, $query_params, null, true );` ## WP-CLI From 8e5680a573d3c1fd0927c8a7b9c633c69e596888 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Mon, 11 Oct 2021 11:09:38 +0100 Subject: [PATCH 5/9] Validate and cast the one-time parameter in the WP-CLI command --- src/CLI/Command.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/CLI/Command.php b/src/CLI/Command.php index c32f9b6..f4b160f 100644 --- a/src/CLI/Command.php +++ b/src/CLI/Command.php @@ -44,13 +44,15 @@ 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; + 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'], $assoc_args['one-time'] ); + $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 ); } From 0a72c17f095cd3794ec7d5ae232cfb8fbc14d46c Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Mon, 11 Oct 2021 11:30:51 +0100 Subject: [PATCH 6/9] Move where we delete one-time keys --- src/AutoLogin.php | 10 ++++++---- src/Model/AutoLoginKey.php | 15 ++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/AutoLogin.php b/src/AutoLogin.php index fe91e65..d394a10 100644 --- a/src/AutoLogin.php +++ b/src/AutoLogin.php @@ -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; } diff --git a/src/Model/AutoLoginKey.php b/src/Model/AutoLoginKey.php index 32251e9..9e4578b 100644 --- a/src/Model/AutoLoginKey.php +++ b/src/Model/AutoLoginKey.php @@ -66,6 +66,8 @@ public function __construct( $attributes = array() ) { if ( ! isset( $attributes['one_time'] ) ) { $this->one_time = false; + } else { + $this->one_time = (bool) $attributes['one_time']; } } @@ -91,29 +93,24 @@ public static function get_by_key( $key ) { return null; } - if ( (bool) $row['one_time'] ) { - self::maybe_delete_one_time_key( $key ); - } - return new self( $row ); } /** - * Static method to check if a key is one-time and to delete it from the - * database if it is. + * Check if the key is one-time and to delete it from the database if it is. * - * @param string $key + * @param AutoLoginKey $key * * @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 static function maybe_delete_one_time_key( $key ) { + 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", $key ) ); + return $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->prefix{$table} WHERE login_key = %s AND one_time = 1", $this->login_key ) ); } /** From 3dee3ca6e89394f89fa97040b057fa417e83b762 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Mon, 11 Oct 2021 12:04:57 +0100 Subject: [PATCH 7/9] Improved comments in WP-CLI command class. --- src/CLI/Command.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CLI/Command.php b/src/CLI/Command.php index f4b160f..e0d4924 100644 --- a/src/CLI/Command.php +++ b/src/CLI/Command.php @@ -47,6 +47,7 @@ public function auto_login_url( $args, $assoc_args ) { // 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.' ); } From aeacb5f2727978d58d7012bee9716a308d9510b6 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Mon, 1 Nov 2021 14:47:17 +0000 Subject: [PATCH 8/9] Remove unnecessary docblock annotation --- src/Model/AutoLoginKey.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Model/AutoLoginKey.php b/src/Model/AutoLoginKey.php index 9e4578b..4cbfd2b 100644 --- a/src/Model/AutoLoginKey.php +++ b/src/Model/AutoLoginKey.php @@ -99,8 +99,6 @@ public static function get_by_key( $key ) { /** * Check if the key is one-time and to delete it from the database if it is. * - * @param AutoLoginKey $key - * * @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. From 8f1229ed051f3ece66e8bc5571ced4fdb3177784 Mon Sep 17 00:00:00 2001 From: Ross Wintle Date: Mon, 1 Nov 2021 14:54:48 +0000 Subject: [PATCH 9/9] Fix bug in instantiation logic This didn't affect operation but was a logic error. --- src/AutoLogin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoLogin.php b/src/AutoLogin.php index d394a10..bab048b 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 = 10_368_000 ) { - 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 ); }