diff --git a/wp-includes/pluggable.php b/wp-includes/pluggable.php index f6c4314b86..59d7fd61b4 100644 --- a/wp-includes/pluggable.php +++ b/wp-includes/pluggable.php @@ -586,6 +586,7 @@ if ( !function_exists('wp_logout') ) : * @since 2.5.0 */ function wp_logout() { + wp_destroy_current_session(); wp_clear_auth_cookie(); /** @@ -631,6 +632,7 @@ function wp_validate_auth_cookie($cookie = '', $scheme = '') { $scheme = $cookie_elements['scheme']; $username = $cookie_elements['username']; $hmac = $cookie_elements['hmac']; + $token = $cookie_elements['token']; $expired = $expiration = $cookie_elements['expiration']; // Allow a grace period for POST and AJAX requests @@ -666,10 +668,10 @@ function wp_validate_auth_cookie($cookie = '', $scheme = '') { $pass_frag = substr($user->user_pass, 8, 4); - $key = wp_hash($username . $pass_frag . '|' . $expiration, $scheme); - $hash = hash_hmac('md5', $username . '|' . $expiration, $key); + $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); + $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key ); - if ( hash_hmac( 'md5', $hmac, $key ) !== hash_hmac( 'md5', $hash, $key ) ) { + if ( hash_hmac( 'sha256', $hmac, $key ) !== hash_hmac( 'sha256', $hash, $key ) ) { /** * Fires if a bad authentication cookie hash is encountered. * @@ -681,7 +683,14 @@ function wp_validate_auth_cookie($cookie = '', $scheme = '') { return false; } - if ( $expiration < time() ) {// AJAX/POST grace period set above + $manager = WP_Session_Tokens::get_instance( $user->ID ); + if ( ! $manager->verify_token( $token ) ) { + do_action( 'auth_cookie_bad_session_token', $cookie_elements ); + return false; + } + + // AJAX/POST grace period set above + if ( $expiration < time() ) { $GLOBALS['login_grace_period'] = 1; } @@ -708,17 +717,26 @@ if ( !function_exists('wp_generate_auth_cookie') ) : * @param int $user_id User ID * @param int $expiration Cookie expiration in seconds * @param string $scheme Optional. The cookie scheme to use: auth, secure_auth, or logged_in - * @return string Authentication cookie contents + * @param string $token User's session token to use for this cookie + * @return string Authentication cookie contents. Empty string if user does not exist. */ -function wp_generate_auth_cookie($user_id, $expiration, $scheme = 'auth') { +function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $token = '' ) { $user = get_userdata($user_id); + if ( ! $user ) { + return ''; + } + + if ( ! $token ) { + $manager = WP_Session_Tokens::get_instance( $user_id ); + $token = $manager->create_token( $expiration ); + } $pass_frag = substr($user->user_pass, 8, 4); - $key = wp_hash($user->user_login . $pass_frag . '|' . $expiration, $scheme); - $hash = hash_hmac('md5', $user->user_login . '|' . $expiration, $key); + $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); + $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key ); - $cookie = $user->user_login . '|' . $expiration . '|' . $hash; + $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash; /** * Filter the authentication cookie. @@ -729,8 +747,9 @@ function wp_generate_auth_cookie($user_id, $expiration, $scheme = 'auth') { * @param int $user_id User ID. * @param int $expiration Authentication cookie expiration in seconds. * @param string $scheme Cookie scheme used. Accepts 'auth', 'secure_auth', or 'logged_in'. + * @param string $token User's session token used. */ - return apply_filters( 'auth_cookie', $cookie, $user_id, $expiration, $scheme ); + return apply_filters( 'auth_cookie', $cookie, $user_id, $expiration, $scheme, $token ); } endif; @@ -772,12 +791,13 @@ function wp_parse_auth_cookie($cookie = '', $scheme = '') { } $cookie_elements = explode('|', $cookie); - if ( count($cookie_elements) != 3 ) + if ( count( $cookie_elements ) !== 4 ) { return false; + } - list($username, $expiration, $hmac) = $cookie_elements; + list( $username, $expiration, $token, $hmac ) = $cookie_elements; - return compact('username', 'expiration', 'hmac', 'scheme'); + return compact( 'username', 'expiration', 'token', 'hmac', 'scheme' ); } endif; @@ -793,6 +813,8 @@ if ( !function_exists('wp_set_auth_cookie') ) : * * @param int $user_id User ID * @param bool $remember Whether to remember the user + * @param mixed $secure Whether the admin cookies should only be sent over HTTPS. + * Default is_ssl(). */ function wp_set_auth_cookie($user_id, $remember = false, $secure = '') { if ( $remember ) { @@ -854,8 +876,11 @@ function wp_set_auth_cookie($user_id, $remember = false, $secure = '') { $scheme = 'auth'; } - $auth_cookie = wp_generate_auth_cookie($user_id, $expiration, $scheme); - $logged_in_cookie = wp_generate_auth_cookie($user_id, $expiration, 'logged_in'); + $manager = WP_Session_Tokens::get_instance( $user_id ); + $token = $manager->create_token( $expiration ); + + $auth_cookie = wp_generate_auth_cookie( $user_id, $expiration, $scheme, $token ); + $logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token ); /** * Fires immediately before the authentication cookie is set. @@ -1682,14 +1707,19 @@ function wp_verify_nonce($nonce, $action = -1) { $uid = apply_filters( 'nonce_user_logged_out', $uid, $action ); } + $token = wp_get_session_token(); $i = wp_nonce_tick(); // Nonce generated 0-12 hours ago - if ( substr(wp_hash($i . $action . $uid, 'nonce'), -12, 10) === $nonce ) + if ( $nonce === substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), -12, 10 ) ) { return 1; + } + // Nonce generated 12-24 hours ago - if ( substr(wp_hash(($i - 1) . $action . $uid, 'nonce'), -12, 10) === $nonce ) + if ( $nonce === substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ) ) { return 2; + } + // Invalid nonce return false; } @@ -1712,9 +1742,10 @@ function wp_create_nonce($action = -1) { $uid = apply_filters( 'nonce_user_logged_out', $uid, $action ); } + $token = wp_get_session_token(); $i = wp_nonce_tick(); - return substr(wp_hash($i . $action . $uid, 'nonce'), -12, 10); + return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ); } endif; diff --git a/wp-includes/session.php b/wp-includes/session.php new file mode 100644 index 0000000000..81a1c694ae --- /dev/null +++ b/wp-includes/session.php @@ -0,0 +1,379 @@ +user_id = $user_id; + } + + /** + * Get a session token manager instance for a user. + * + * This method contains a filter that allows a plugin to swap out + * the session manager for a subclass of WP_Session_Tokens. + * + * @since 4.0.0 + * + * @param int $user_id User whose session to manage. + */ + final public static function get_instance( $user_id ) { + /** + * Filter the session token manager used. + * + * @since 4.0.0 + * + * @param string $session Name of class to use as the manager. + * Default 'WP_User_Meta_Session_Tokens'. + */ + $manager = apply_filters( 'session_token_manager', 'WP_User_Meta_Session_Tokens' ); + return new $manager( $user_id ); + } + + /** + * Hashes a token for storage. + * + * @since 4.0.0 + * + * @param string $token Token to hash. + * @return string A hash of the token (a verifier). + */ + final private function hash_token( $token ) { + return hash( 'sha256', $token ); + } + + /** + * Validate a user's session token as authentic. + * + * Checks that the given token is present and hasn't expired. + * + * @since 4.0.0 + * + * @param string $token Token to verify. + * @return bool Whether the token is valid for the user. + */ + final public function verify_token( $token ) { + $verifier = $this->hash_token( $token ); + return (bool) $this->get_session( $verifier ); + } + + /** + * Generate a cookie session identification token. + * + * A session identification token is a long, random string. It is used to + * link a cookie to an expiration time and to ensure that cookies become + * invalidated upon logout. This function generates a token and stores it + * with the associated expiration time. + * + * @since 4.0.0 + * + * @param int $expiration Session expiration timestamp. + * @return string Session identification token. + */ + final public function create_token( $expiration ) { + /** + * Filter the information attached to the newly created session. + * + * Could be used in the future to attach information such as + * IP address or user agent to a session. + * + * @since 4.0.0 + * + * @param array $session Array of extra data. + * @param int $user_id User ID. + */ + $session = apply_filters( 'attach_session_information', array(), $this->user_id ); + $session['expiration'] = $expiration; + + $token = wp_generate_password( 43, false, false ); + + $this->update_token( $token, $session ); + + return $token; + } + + /** + * Updates a session based on its token. + * + * @since 4.0.0 + * + * @param string $token Token to update. + * @param array $session Session information. + */ + final public function update_token( $token, $session ) { + $verifier = $this->hash_token( $token ); + $this->update_session( $verifier, $session ); + } + + /** + * Destroy a session token. + * + * @since 4.0.0 + * + * @param string $token Token to destroy. + */ + final public function destroy_token( $token ) { + $verifier = $this->hash_token( $token ); + $this->update_session( $verifier, null ); + } + + /** + * Destroy all session tokens for this user, + * except a single token, presumably the one in use. + * + * @since 4.0.0 + * + * @param string $token_to_keep Token to keep. + */ + final public function destroy_other_tokens( $token_to_keep ) { + $verifier = $this->hash_token( $token_to_keep ); + $session = $this->get_session( $verifier ); + if ( $session ) { + $this->destroy_other_sessions( $verifier ); + } else { + $this->destroy_all_tokens(); + } + } + + /** + * Determine whether a session token is still valid, + * based on expiration. + * + * @since 4.0.0 + * + * @param array $session Session to check. + * @return bool Whether session is valid. + */ + final protected function is_still_valid( $session ) { + return $session['expiration'] >= time(); + } + + /** + * Destroy all tokens for a user. + * + * @since 4.0.0 + */ + final public function destroy_all_tokens() { + $this->destroy_all_sessions(); + } + + /** + * Destroy all tokens for all users. + * + * @since 4.0.0 + */ + final public static function destroy_all_tokens_for_all_users() { + $manager = apply_filters( 'session_token_manager', 'WP_User_Meta_Session_Tokens' ); + $manager::drop_sessions(); + } + + /** + * Retrieve all sessions of a user. + * + * @since 4.0.0 + * + * @return array Sessions of a user. + */ + final public function get_all_sessions() { + return array_values( $this->get_sessions() ); + } + + /** + * This method should retrieve all sessions of a user, keyed by verifier. + * + * @since 4.0.0 + * + * @return array Sessions of a user, keyed by verifier. + */ + abstract protected function get_sessions(); + + /** + * This method should look up a session by its verifier (token hash). + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to retrieve. + * @return array|null The session, or null if it does not exist. + */ + abstract protected function get_session( $verifier ); + + /** + * This method should update a session by its verifier. + * + * Omitting the second argument should destroy the session. + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to update. + */ + abstract protected function update_session( $verifier, $session = null ); + + /** + * This method should destroy all session tokens for this user, + * except a single session passed. + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to keep. + */ + abstract protected function destroy_other_sessions( $verifier ); + + /** + * This method should destroy all sessions for a user. + * + * @since 4.0.0 + */ + abstract protected function destroy_all_sessions(); + + /** + * This static method should destroy all session tokens for all users. + * + * @since 4.0.0 + */ + abstract public static function drop_sessions(); +} + +/** + * Meta-based user sessions token manager. + * + * @since 4.0.0 + */ +class WP_User_Meta_Session_Tokens extends WP_Session_Tokens { + + /** + * Get all sessions of a user. + * + * @since 4.0.0 + * + * @return array Sessions of a user. + */ + protected function get_sessions() { + $sessions = get_user_meta( $this->user_id, 'session_tokens', true ); + + if ( ! is_array( $sessions ) ) { + return array(); + } + + $sessions = array_map( array( $this, 'prepare_session' ), $sessions ); + return array_filter( $sessions, array( $this, 'is_still_valid' ) ); + } + + /** + * Converts an expiration to an array of session information. + * + * @param mixed $session Session or expiration. + * @return array Session. + */ + protected function prepare_session( $session ) { + if ( is_int( $session ) ) { + return array( 'expiration' => $session ); + } + + return $session; + } + + /** + * Retrieve a session by its verifier (token hash). + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to retrieve. + * @return array|null The session, or null if it does not exist + */ + protected function get_session( $verifier ) { + $sessions = $this->get_sessions(); + + if ( isset( $sessions[ $verifier ] ) ) { + return $sessions[ $verifier ]; + } + + return null; + } + + /** + * Update a session by its verifier. + * + * Omitting the second argument destroys the session. + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to update. + */ + protected function update_session( $verifier, $session = null ) { + $sessions = $this->get_sessions(); + + if ( $session ) { + $sessions[ $verifier ] = $session; + } else { + unset( $sessions[ $verifier ] ); + } + + $this->update_sessions( $sessions ); + } + + /** + * Update a user's sessions in the usermeta table. + * + * @since 4.0.0 + * + * @param array $sessions + */ + protected function update_sessions( $sessions ) { + if ( ! has_filter( 'attach_session_information' ) ) { + $sessions = wp_list_pluck( $sessions, 'expiration' ); + } + + if ( $sessions ) { + update_user_meta( $this->user_id, 'session_tokens', $sessions ); + } else { + delete_user_meta( $this->user_id, 'session_tokens' ); + } + } + + /** + * Destroy all session tokens for a user, except a single session passed. + * + * @since 4.0.0 + * + * @param $verifier Verifier of the session to keep. + */ + protected function destroy_other_sessions( $verifier ) { + $session = $this->get_session( $verifier ); + $this->update_sessions( array( $verifier => $session ) ); + } + + /** + * Destroy all session tokens for a user. + * + * @since 4.0.0 + */ + protected function destroy_all_sessions() { + $this->update_sessions( array() ); + } + + /** + * Destroy all session tokens for all users. + * + * @since 4.0.0 + */ + public static function drop_sessions() { + delete_metadata( 'user', false, 'session_tokens', false, true ); + } +} diff --git a/wp-includes/user.php b/wp-includes/user.php index f70cc2676b..ccc3980664 100644 --- a/wp-includes/user.php +++ b/wp-includes/user.php @@ -2173,3 +2173,62 @@ function register_new_user( $user_login, $user_email ) { return $user_id; } + +/** + * Retrieve the current session token from the logged_in cookie. + * + * @since 4.0.0 + * + * @return string Token. + */ +function wp_get_session_token() { + $cookie = wp_parse_auth_cookie( '', 'logged_in' ); + return ! empty( $cookie['token'] ) ? $cookie['token'] : ''; +} + +/** + * Retrieve a list of sessions for the current user. + * + * @since 4.0.0 + * @return array Array of sessions. + */ +function wp_get_all_sessions() { + $manager = WP_Session_Tokens::get_instance( get_current_user_id() ); + return $manager->get_all_sessions(); +} + +/** + * Remove the current session token from the database. + * + * @since 4.0.0 + */ +function wp_destroy_current_session() { + $token = wp_get_session_token(); + if ( $token ) { + $manager = WP_Session_Tokens::get_instance( get_current_user_id() ); + $manager->destroy_token( $token ); + } +} + +/** + * Remove all but the current session token for the current user for the database. + * + * @since 4.0.0 + */ +function wp_destroy_other_sessions() { + $token = wp_get_session_token(); + if ( $token ) { + $manager = WP_Session_Tokens::get_instance( get_current_user_id() ); + $manager->destroy_other_tokens( $token ); + } +} + +/** + * Remove all session tokens for the current user from the database. + * + * @since 4.0.0 + */ +function wp_destroy_all_sessions() { + $manager = WP_Session_Tokens::get_instance( get_current_user_id() ); + $manager->destroy_all_tokens(); +} diff --git a/wp-settings.php b/wp-settings.php index 9f73195023..97959716b3 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -119,6 +119,7 @@ require( ABSPATH . WPINC . '/theme.php' ); require( ABSPATH . WPINC . '/class-wp-theme.php' ); require( ABSPATH . WPINC . '/template.php' ); require( ABSPATH . WPINC . '/user.php' ); +require( ABSPATH . WPINC . '/session.php' ); require( ABSPATH . WPINC . '/meta.php' ); require( ABSPATH . WPINC . '/general-template.php' ); require( ABSPATH . WPINC . '/link-template.php' );