expiry timestamp. */ const META = '_adminkeep_trusted_devices'; /** * Trust the current browser. * * @param int $user_id The user. * @param int $days How long for. * @return string The cookie value, "|". */ public static function issue( $user_id, $days ) { $token = bin2hex( random_bytes( 16 ) ); $devices = self::devices( $user_id ); $devices[ self::hash( $token ) ] = time() + max( 1, (int) $days ) * DAY_IN_SECONDS; update_user_meta( $user_id, self::META, $devices ); return (int) $user_id . '|' . $token; } /** * Does this cookie prove this user's browser? * * The user id travels in the cookie only so a token issued to one user is never looked up * against another; the match is still on the hash in that user's own meta. * * @param int $user_id The user logging in. * @param string $cookie_value The cookie, raw. * @return bool */ public static function is_trusted( $user_id, $cookie_value ) { if ( ! is_string( $cookie_value ) || 1 !== preg_match( '/^(\d+)\|([0-9a-f]{32})$/', $cookie_value, $m ) ) { return false; } if ( (int) $m[1] !== (int) $user_id ) { return false; } $devices = self::devices( $user_id ); $hash = self::hash( $m[2] ); if ( ! isset( $devices[ $hash ] ) ) { return false; } if ( $devices[ $hash ] < time() ) { // Prune on the way out, so meta does not collect dead tokens. unset( $devices[ $hash ] ); update_user_meta( $user_id, self::META, $devices ); return false; } return true; } /** * Forget every device, so the next login on each asks for a code again. * * @param int $user_id The user. * @return void */ public static function revoke_all( $user_id ) { delete_user_meta( $user_id, self::META ); } /** * Send the cookie, on the path wp-login.php can see. * * @param string $value The value from issue(). * @param int $days Lifetime. * @return void */ public static function set_cookie( $value, $days ) { // Only wp-login.php reads this cookie, and it lives under the site URL, whose path can differ from the home path that COOKIEPATH holds. setcookie( self::COOKIE, $value, time() + max( 1, (int) $days ) * DAY_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } /** * Stored devices, well-formed entries only; expiry is checked in is_trusted(). * * @param int $user_id The user. * @return array hash => expiry. */ private static function devices( $user_id ) { $devices = get_user_meta( $user_id, self::META, true ); if ( ! is_array( $devices ) ) { return array(); } return array_filter( $devices, static function ( $expires ) { return is_int( $expires ); } ); } /** * The stored form of a token. * * @param string $token The token. * @return string */ private static function hash( $token ) { return wp_hash( 'trusted-device:' . $token, 'auth' ); } }