Changeset 59828 for trunk/tests/phpunit/tests/auth.php
- Timestamp:
- 02/17/2025 11:22:33 AM (3 months ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/tests/phpunit/tests/auth.php
r59595 r59828 11 11 const USER_PASS = 'password'; 12 12 13 /** 14 * @var WP_User 15 */ 13 16 protected $user; 14 17 … … 17 20 */ 18 21 protected static $_user; 22 23 /** 24 * @var int 25 */ 19 26 protected static $user_id; 27 28 /** 29 * @var PasswordHash 30 */ 20 31 protected static $wp_hasher; 32 33 protected static $bcrypt_length_limit = 72; 34 35 protected static $phpass_length_limit = 4096; 36 37 protected static $password_length_limit = 4096; 21 38 22 39 /** … … 90 107 $cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'foo' ); 91 108 $this->assertFalse( wp_validate_auth_cookie( $cookie, 'bar' ) ); 109 } 110 111 /** 112 * @ticket 21022 113 */ 114 public function test_auth_cookie_generated_with_phpass_hash_remains_valid() { 115 self::set_user_password_with_phpass( 'password', self::$user_id ); 116 117 $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' ); 118 119 $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) ); 120 } 121 122 /** 123 * @ticket 21022 124 */ 125 public function test_auth_cookie_generated_with_plain_bcrypt_hash_remains_valid() { 126 self::set_user_password_with_plain_bcrypt( 'password', self::$user_id ); 127 128 $auth_cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' ); 129 130 $this->assertSame( self::$user_id, wp_validate_auth_cookie( $auth_cookie, 'auth' ) ); 92 131 } 93 132 … … 107 146 $authed_user = wp_authenticate( $this->user->user_login, $password_to_test ); 108 147 148 $this->assertNotWPError( $authed_user ); 109 149 $this->assertInstanceOf( 'WP_User', $authed_user ); 110 150 $this->assertSame( $this->user->ID, $authed_user->ID ); … … 158 198 $password = "pass with vertical tab o_O\x0B"; 159 199 $this->assertTrue( wp_check_password( 'pass with vertical tab o_O', wp_hash_password( $password ) ) ); 200 } 201 202 /** 203 * @ticket 21022 204 */ 205 public function test_wp_check_password_supports_phpass_hash() { 206 $password = 'password'; 207 $hash = self::$wp_hasher->HashPassword( $password ); 208 $this->assertTrue( wp_check_password( $password, $hash ) ); 209 $this->assertSame( 1, did_filter( 'check_password' ) ); 210 } 211 212 /** 213 * Ensure wp_check_password() remains compatible with an increase to the default bcrypt cost. 214 * 215 * The test verifies this by reducing the cost used to generate the hash, therefore mimicing a hash 216 * which was generated prior to the default cost being increased. 217 * 218 * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 . 219 * 220 * @ticket 21022 221 */ 222 public function test_wp_check_password_supports_hash_with_increased_bcrypt_cost() { 223 $password = 'password'; 224 225 // Reducing the cost mimics an increase to the default cost. 226 add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 227 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 228 remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 229 230 $this->assertTrue( wp_check_password( $password, $hash ) ); 231 $this->assertSame( 1, did_filter( 'check_password' ) ); 232 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 233 } 234 235 /** 236 * Ensure wp_check_password() remains compatible with a reduction of the default bcrypt cost. 237 * 238 * The test verifies this by increasing the cost used to generate the hash, therefore mimicing a hash 239 * which was generated prior to the default cost being reduced. 240 * 241 * A reduction of the cost is unlikely to occur but is fully supported. 242 * 243 * @ticket 21022 244 */ 245 public function test_wp_check_password_supports_hash_with_reduced_bcrypt_cost() { 246 $password = 'password'; 247 248 // Increasing the cost mimics a reduction of the default cost. 249 add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); 250 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 251 remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); 252 253 $this->assertTrue( wp_check_password( $password, $hash ) ); 254 $this->assertSame( 1, did_filter( 'check_password' ) ); 255 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 256 } 257 258 /** 259 * @ticket 21022 260 */ 261 public function test_wp_check_password_supports_wp_hash_with_default_bcrypt_cost() { 262 $password = 'password'; 263 264 $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); 265 266 $this->assertTrue( wp_check_password( $password, $hash ) ); 267 $this->assertSame( 1, did_filter( 'check_password' ) ); 268 $this->assertFalse( wp_password_needs_rehash( $hash ) ); 269 } 270 271 /** 272 * @ticket 21022 273 */ 274 public function test_wp_check_password_supports_plain_bcrypt_hash_with_default_bcrypt_cost() { 275 $password = 'password'; 276 277 $hash = password_hash( $password, PASSWORD_BCRYPT ); 278 279 $this->assertTrue( wp_check_password( $password, $hash ) ); 280 $this->assertSame( 1, did_filter( 'check_password' ) ); 281 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 282 } 283 284 /** 285 * Ensure wp_check_password() is compatible with Argon2i hashes. 286 * 287 * @ticket 21022 288 */ 289 public function test_wp_check_password_supports_argon2i_hash() { 290 if ( ! defined( 'PASSWORD_ARGON2I' ) ) { 291 $this->fail( 'Argon2i is not supported.' ); 292 } 293 294 $password = 'password'; 295 $hash = password_hash( trim( $password ), PASSWORD_ARGON2I ); 296 $this->assertTrue( wp_check_password( $password, $hash ) ); 297 $this->assertSame( 1, did_filter( 'check_password' ) ); 298 } 299 300 /** 301 * Ensure wp_check_password() is compatible with Argon2id hashes. 302 * 303 * @requires PHP >= 7.3 304 * 305 * @ticket 21022 306 */ 307 public function test_wp_check_password_supports_argon2id_hash() { 308 if ( ! defined( 'PASSWORD_ARGON2ID' ) ) { 309 $this->fail( 'Argon2id is not supported.' ); 310 } 311 312 $password = 'password'; 313 $hash = password_hash( trim( $password ), PASSWORD_ARGON2ID ); 314 $this->assertTrue( wp_check_password( $password, $hash ) ); 315 $this->assertSame( 1, did_filter( 'check_password' ) ); 316 } 317 318 /** 319 * @ticket 21022 320 */ 321 public function test_wp_check_password_does_not_support_md5_hashes() { 322 $password = 'password'; 323 $hash = md5( $password ); 324 $this->assertFalse( wp_check_password( $password, $hash ) ); 325 $this->assertSame( 1, did_filter( 'check_password' ) ); 326 } 327 328 /** 329 * @ticket 21022 330 */ 331 public function test_wp_check_password_does_not_support_plain_text() { 332 $password = 'password'; 333 $hash = $password; 334 $this->assertFalse( wp_check_password( $password, $hash ) ); 335 $this->assertSame( 1, did_filter( 'check_password' ) ); 336 } 337 338 /** 339 * @ticket 21022 340 * 341 * @dataProvider data_empty_values 342 * @param mixed $value 343 */ 344 public function test_wp_check_password_does_not_support_empty_hash( $value ) { 345 $password = 'password'; 346 $hash = $value; 347 $this->assertFalse( wp_check_password( $password, $hash ) ); 348 $this->assertSame( 1, did_filter( 'check_password' ) ); 349 } 350 351 /** 352 * @ticket 21022 353 * 354 * @dataProvider data_empty_values 355 * @param mixed $value 356 */ 357 public function test_wp_check_password_does_not_support_empty_password( $value ) { 358 $password = $value; 359 $hash = $value; 360 $this->assertFalse( wp_check_password( $password, $hash ) ); 361 $this->assertSame( 1, did_filter( 'check_password' ) ); 362 } 363 364 public function data_empty_values() { 365 return array( 366 // Integer zero: 367 array( 0 ), 368 // String zero: 369 array( '0' ), 370 // Zero-length string: 371 array( '' ), 372 // Null byte character: 373 array( "\0" ), 374 // Asterisk values: 375 array( '*' ), 376 array( '*0' ), 377 array( '*1' ), 378 ); 160 379 } 161 380 … … 236 455 } 237 456 238 public function test_password_length_limit() { 239 $limit = str_repeat( 'a', 4096 ); 240 457 /** 458 * @ticket 21022 459 */ 460 public function test_password_is_hashed_with_bcrypt() { 461 $password = 'password'; 462 463 // Set the user password. 464 wp_set_password( $password, self::$user_id ); 465 466 // Ensure the password is hashed with bcrypt. 467 $this->assertStringStartsWith( '$wp$2y$', get_userdata( self::$user_id )->user_pass ); 468 469 // Authenticate. 470 $user = wp_authenticate( $this->user->user_login, $password ); 471 472 // Verify correct password. 473 $this->assertNotWPError( $user ); 474 $this->assertInstanceOf( 'WP_User', $user ); 475 $this->assertSame( self::$user_id, $user->ID ); 476 } 477 478 /** 479 * @ticket 21022 480 */ 481 public function test_invalid_password_at_bcrypt_length_limit_is_rejected() { 482 $limit = str_repeat( 'a', self::$bcrypt_length_limit ); 483 484 // Set the user password to the bcrypt limit. 241 485 wp_set_password( $limit, self::$user_id ); 242 // phpass hashed password.243 $this->assertStringStartsWith( '$P$', $this->user->data->user_pass );244 486 245 487 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 246 488 // Wrong password. 247 $this->assertInstanceOf( 'WP_Error', $user ); 248 489 $this->assertWPError( $user ); 490 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 491 } 492 493 /** 494 * @ticket 21022 495 */ 496 public function test_invalid_password_beyond_bcrypt_length_limit_is_rejected() { 497 $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 498 499 // Set the user password beyond the bcrypt limit. 500 wp_set_password( $limit, self::$user_id ); 501 502 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 503 // Wrong password. 504 $this->assertWPError( $user ); 505 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 506 } 507 508 /** 509 * @ticket 21022 510 */ 511 public function test_valid_password_at_bcrypt_length_limit_is_accepted() { 512 $limit = str_repeat( 'a', self::$bcrypt_length_limit ); 513 514 // Set the user password to the bcrypt limit. 515 wp_set_password( $limit, self::$user_id ); 516 517 // Authenticate. 249 518 $user = wp_authenticate( $this->user->user_login, $limit ); 519 520 // Correct password. 521 $this->assertNotWPError( $user ); 250 522 $this->assertInstanceOf( 'WP_User', $user ); 251 523 $this->assertSame( self::$user_id, $user->ID ); 252 253 // One char too many. 254 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 524 } 525 526 /** 527 * @ticket 21022 528 */ 529 public function test_valid_password_beyond_bcrypt_length_limit_is_accepted() { 530 $limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 531 532 // Set the user password beyond the bcrypt limit. 533 wp_set_password( $limit, self::$user_id ); 534 535 // Authenticate. 536 $user = wp_authenticate( $this->user->user_login, $limit ); 537 538 // Correct password depite its length. 539 $this->assertNotWPError( $user ); 540 $this->assertInstanceOf( 'WP_User', $user ); 541 $this->assertSame( self::$user_id, $user->ID ); 542 } 543 544 /** 545 * A password beyond 72 bytes will be truncated by bcrypt by default and still be accepted. 546 * 547 * This ensures that a truncated password is not accepted by WordPress. 548 * 549 * @ticket 21022 550 */ 551 public function test_long_truncated_password_is_rejected() { 552 $at_limit = str_repeat( 'a', self::$bcrypt_length_limit ); 553 $beyond_limit = str_repeat( 'a', self::$bcrypt_length_limit + 1 ); 554 555 // Set the user password beyond the bcrypt limit. 556 wp_set_password( $beyond_limit, self::$user_id ); 557 558 // Authenticate using a truncated password. 559 $user = wp_authenticate( $this->user->user_login, $at_limit ); 560 561 // Incorrect password. 562 $this->assertWPError( $user ); 563 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 564 } 565 566 /** 567 * @ticket 21022 568 */ 569 public function test_setting_password_beyond_bcrypt_length_limit_is_rejected() { 570 $beyond_limit = str_repeat( 'a', self::$password_length_limit + 1 ); 571 572 // Set the user password beyond the limit. 573 wp_set_password( $beyond_limit, self::$user_id ); 574 575 // Password broken by setting it to be too long. 576 $user = get_user_by( 'id', self::$user_id ); 577 $this->assertSame( '*', $user->data->user_pass ); 578 579 // Password is not accepted. 580 $user = wp_authenticate( $this->user->user_login, $beyond_limit ); 581 $this->assertInstanceOf( 'WP_Error', $user ); 582 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 583 584 // Placeholder is not accepted. 585 $user = wp_authenticate( $this->user->user_login, '*' ); 586 $this->assertInstanceOf( 'WP_Error', $user ); 587 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 588 } 589 590 /** 591 * @see https://core.trac.wordpress.org/changeset/30466 592 */ 593 public function test_invalid_password_at_phpass_length_limit_is_rejected() { 594 $limit = str_repeat( 'a', self::$phpass_length_limit ); 595 596 // Set the user password with the old phpass algorithm. 597 self::set_user_password_with_phpass( $limit, self::$user_id ); 598 599 // Authenticate. 600 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 601 255 602 // Wrong password. 256 603 $this->assertInstanceOf( 'WP_Error', $user ); 257 258 wp_set_password( $limit . 'a', self::$user_id ); 604 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 605 } 606 607 public function test_valid_password_at_phpass_length_limit_is_accepted() { 608 $limit = str_repeat( 'a', self::$phpass_length_limit ); 609 610 // Set the user password with the old phpass algorithm. 611 self::set_user_password_with_phpass( $limit, self::$user_id ); 612 613 // Authenticate. 614 $user = wp_authenticate( $this->user->user_login, $limit ); 615 616 // Correct password. 617 $this->assertNotWPError( $user ); 618 $this->assertInstanceOf( 'WP_User', $user ); 619 $this->assertSame( self::$user_id, $user->ID ); 620 } 621 622 public function test_too_long_password_at_phpass_length_limit_is_rejected() { 623 $limit = str_repeat( 'a', self::$phpass_length_limit ); 624 625 // Set the user password with the old phpass algorithm. 626 self::set_user_password_with_phpass( $limit, self::$user_id ); 627 628 // Authenticate with a password that is one character too long. 629 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 630 631 // Wrong password. 632 $this->assertInstanceOf( 'WP_Error', $user ); 633 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 634 } 635 636 public function test_too_long_password_beyond_phpass_length_limit_is_rejected() { 637 // One char too many. 638 $too_long = str_repeat( 'a', self::$phpass_length_limit + 1 ); 639 640 // Set the user password with the old phpass algorithm. 641 self::set_user_password_with_phpass( $too_long, self::$user_id ); 642 259 643 $user = get_user_by( 'id', self::$user_id ); 260 644 // Password broken by setting it to be too long. 261 645 $this->assertSame( '*', $user->data->user_pass ); 262 646 647 // Password is not accepted. 263 648 $user = wp_authenticate( $this->user->user_login, '*' ); 264 649 $this->assertInstanceOf( 'WP_Error', $user ); 265 266 $user = wp_authenticate( $this->user->user_login, '*0' ); 650 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 651 } 652 653 /** 654 * @dataProvider data_empty_values 655 * @param mixed $value 656 */ 657 public function test_empty_password_is_rejected_by_bcrypt( $value ) { 658 // Set the user password. 659 wp_set_password( 'password', self::$user_id ); 660 661 $user = wp_authenticate( $this->user->user_login, $value ); 267 662 $this->assertInstanceOf( 'WP_Error', $user ); 268 269 $user = wp_authenticate( $this->user->user_login, '*1' ); 663 } 664 665 /** 666 * @dataProvider data_empty_values 667 * @param mixed $value 668 */ 669 public function test_empty_password_is_rejected_by_phpass( $value ) { 670 // Set the user password with the old phpass algorithm. 671 self::set_user_password_with_phpass( 'password', self::$user_id ); 672 673 $user = wp_authenticate( $this->user->user_login, $value ); 270 674 $this->assertInstanceOf( 'WP_Error', $user ); 675 } 676 677 public function test_incorrect_password_is_rejected_by_phpass() { 678 // Set the user password with the old phpass algorithm. 679 self::set_user_password_with_phpass( 'password', self::$user_id ); 271 680 272 681 $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); 682 273 683 // Wrong password. 274 684 $this->assertInstanceOf( 'WP_Error', $user ); 275 276 $user = wp_authenticate( $this->user->user_login, $limit ); 277 // Wrong password. 278 $this->assertInstanceOf( 'WP_Error', $user ); 685 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 686 } 687 688 public function test_too_long_password_is_rejected_by_phpass() { 689 $limit = str_repeat( 'a', self::$phpass_length_limit ); 690 691 // Set the user password with the old phpass algorithm. 692 self::set_user_password_with_phpass( 'password', self::$user_id ); 279 693 280 694 $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); 695 281 696 // Password broken by setting it to be too long. 282 697 $this->assertInstanceOf( 'WP_Error', $user ); 698 $this->assertSame( 'incorrect_password', $user->get_error_code() ); 283 699 } 284 700 … … 307 723 $wpdb->users, 308 724 array( 309 'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ),725 'user_activation_key' => strtotime( '-1 hour' ) . ':' . wp_fast_hash( $key ), 310 726 ), 311 727 array( … … 345 761 $wpdb->users, 346 762 array( 347 'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ),763 'user_activation_key' => strtotime( '-48 hours' ) . ':' . wp_fast_hash( $key ), 348 764 ), 349 765 array( … … 356 772 $check = check_password_reset_key( $key, $this->user->user_login ); 357 773 $this->assertInstanceOf( 'WP_Error', $check ); 774 $this->assertSame( 'expired_key', $check->get_error_code() ); 358 775 } 359 776 … … 394 811 $check = check_password_reset_key( $key, $this->user->user_login ); 395 812 $this->assertInstanceOf( 'WP_Error', $check ); 813 $this->assertSame( 'expired_key', $check->get_error_code() ); 396 814 397 815 // An empty key with a legacy user_activation_key should be rejected. 398 816 $check = check_password_reset_key( '', $this->user->user_login ); 399 817 $this->assertInstanceOf( 'WP_Error', $check ); 818 $this->assertSame( 'invalid_key', $check->get_error_code() ); 819 } 820 821 /** 822 * @ticket 21022 823 */ 824 public function test_phpass_user_activation_key_is_allowed() { 825 global $wpdb; 826 827 // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0. 828 829 $key = wp_generate_password( 20, false ); 830 $wpdb->update( 831 $wpdb->users, 832 array( 833 'user_activation_key' => strtotime( '-1 hour' ) . ':' . self::$wp_hasher->HashPassword( $key ), 834 ), 835 array( 836 'ID' => $this->user->ID, 837 ) 838 ); 839 clean_user_cache( $this->user ); 840 841 // A legacy phpass user_activation_key should remain valid. 842 $check = check_password_reset_key( $key, $this->user->user_login ); 843 $this->assertNotWPError( $check ); 844 $this->assertInstanceOf( 'WP_User', $check ); 845 $this->assertSame( $this->user->ID, $check->ID ); 846 847 // An empty key with a legacy user_activation_key should be rejected. 848 $check = check_password_reset_key( '', $this->user->user_login ); 849 $this->assertWPError( $check ); 850 $this->assertSame( 'invalid_key', $check->get_error_code() ); 851 } 852 853 /** 854 * @ticket 21022 855 */ 856 public function test_expired_phpass_user_activation_key_is_rejected() { 857 global $wpdb; 858 859 // A legacy user_activation_key is one hashed using phpass between WordPress 4.3 and 6.8.0. 860 861 $key = wp_generate_password( 20, false ); 862 $wpdb->update( 863 $wpdb->users, 864 array( 865 'user_activation_key' => strtotime( '-48 hours' ) . ':' . self::$wp_hasher->HashPassword( $key ), 866 ), 867 array( 868 'ID' => $this->user->ID, 869 ) 870 ); 871 clean_user_cache( $this->user ); 872 873 // A legacy phpass user_activation_key should still be subject to an expiry check. 874 $check = check_password_reset_key( $key, $this->user->user_login ); 875 $this->assertWPError( $check ); 876 $this->assertSame( 'expired_key', $check->get_error_code() ); 877 878 // An empty key with a legacy user_activation_key should be rejected. 879 $check = check_password_reset_key( '', $this->user->user_login ); 880 $this->assertWPError( $check ); 881 $this->assertSame( 'invalid_key', $check->get_error_code() ); 882 } 883 884 /** 885 * @ticket 21022 886 */ 887 public function test_user_request_key_handling() { 888 $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' ); 889 $key = wp_generate_user_request_key( $request_id ); 890 891 // A valid key should be accepted. 892 $check = wp_validate_user_request_key( $request_id, $key ); 893 $this->assertNotWPError( $check ); 894 $this->assertTrue( $check ); 895 896 // An invalid key should rejected. 897 $check = wp_validate_user_request_key( $request_id, 'invalid' ); 898 $this->assertWPError( $check ); 899 $this->assertSame( 'invalid_key', $check->get_error_code() ); 900 901 // An empty key should be rejected. 902 $check = wp_validate_user_request_key( $request_id, '' ); 903 $this->assertWPError( $check ); 904 $this->assertSame( 'missing_key', $check->get_error_code() ); 905 } 906 907 /** 908 * @ticket 21022 909 */ 910 public function test_phpass_user_request_key_is_allowed() { 911 // A legacy user request key is one hashed using phpass between WordPress 4.3 and 6.8.0. 912 913 $request_id = wp_create_user_request( 'test@example.com', 'remove_personal_data' ); 914 $key = wp_generate_password( 20, false ); 915 916 wp_update_post( 917 array( 918 'ID' => $request_id, 919 'post_password' => self::$wp_hasher->HashPassword( $key ), 920 ) 921 ); 922 923 // A legacy phpass key should remain valid. 924 $check = wp_validate_user_request_key( $request_id, $key ); 925 $this->assertNotWPError( $check ); 926 $this->assertTrue( $check ); 927 928 // An empty key with a legacy key should be rejected. 929 $check = wp_validate_user_request_key( $request_id, '' ); 930 $this->assertWPError( $check ); 931 $this->assertSame( 'missing_key', $check->get_error_code() ); 932 } 933 934 /** 935 * The `wp_password_needs_rehash()` function is just a wrapper around `password_needs_rehash()`, but this ensures 936 * that it works as expected. 937 * 938 * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 . 939 * 940 * @ticket 21022 941 */ 942 public function check_password_needs_rehashing() { 943 $password = 'password'; 944 945 // Current password hashing algorithm. 946 $hash = wp_hash_password( $password ); 947 $this->assertFalse( wp_password_needs_rehash( $hash ) ); 948 949 // A future upgrade from a previously lower cost. 950 $default = self::get_default_bcrypt_cost(); 951 $opts = array( 952 // Reducing the cost mimics an increase in the default cost. 953 'cost' => $default - 1, 954 ); 955 $hash = password_hash( $password, PASSWORD_BCRYPT, $opts ); 956 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 957 958 // Previous phpass algorithm. 959 $hash = self::$wp_hasher->HashPassword( $password ); 960 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 961 962 // o_O md5. 963 $hash = md5( $password ); 964 $this->assertTrue( wp_password_needs_rehash( $hash ) ); 400 965 } 401 966 … … 456 1021 $this->assertEmpty( $user->user_activation_key, 'The `user_activation_key` was not empty on the user object returned by `wp_signon()` function.' ); 457 1022 $this->assertEmpty( $activation_key_from_database, 'The `user_activation_key` was not empty in the database.' ); 1023 } 1024 1025 /** 1026 * @ticket 21022 1027 */ 1028 public function test_phpass_application_password_is_accepted() { 1029 add_filter( 'application_password_is_api_request', '__return_true' ); 1030 add_filter( 'wp_is_application_passwords_available', '__return_true' ); 1031 1032 $password = 'password'; 1033 1034 // Set an application password with the old phpass algorithm. 1035 $uuid = self::set_application_password_with_phpass( $password, self::$user_id ); 1036 1037 // Authenticate. 1038 $user = wp_authenticate_application_password( null, self::USER_LOGIN, $password ); 1039 1040 // Verify that the phpass hash for the application password was valid. 1041 $this->assertNotWPError( $user ); 1042 $this->assertInstanceOf( 'WP_User', $user ); 1043 $this->assertSame( self::$user_id, $user->ID ); 1044 } 1045 1046 /** 1047 * @dataProvider data_usernames 1048 * 1049 * @ticket 21022 1050 */ 1051 public function test_phpass_password_is_rehashed_after_successful_user_password_authentication( $username_or_email ) { 1052 $password = 'password'; 1053 1054 // Set the user password with the old phpass algorithm. 1055 self::set_user_password_with_phpass( $password, self::$user_id ); 1056 1057 // Verify that the password needs rehashing. 1058 $hash = get_userdata( self::$user_id )->user_pass; 1059 $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) ); 1060 1061 // Authenticate. 1062 $user = wp_authenticate( $username_or_email, $password ); 1063 1064 // Verify that the phpass password hash was valid. 1065 $this->assertNotWPError( $user ); 1066 $this->assertInstanceOf( 'WP_User', $user ); 1067 $this->assertSame( self::$user_id, $user->ID ); 1068 1069 // Verify that the password no longer needs rehashing. 1070 $hash = get_userdata( self::$user_id )->user_pass; 1071 $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) ); 1072 1073 // Authenticate a second time to ensure the new hash is valid. 1074 $user = wp_authenticate( $username_or_email, $password ); 1075 1076 // Verify that the bcrypt password hash is valid. 1077 $this->assertNotWPError( $user ); 1078 $this->assertInstanceOf( 'WP_User', $user ); 1079 $this->assertSame( self::$user_id, $user->ID ); 1080 } 1081 1082 /** 1083 * @dataProvider data_usernames 1084 * 1085 * @ticket 21022 1086 */ 1087 public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_user_password_authentication( $username_or_email ) { 1088 $password = 'password'; 1089 1090 // Hash the user password with a lower cost than default to mimic a cost upgrade. 1091 add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 1092 wp_set_password( $password, self::$user_id ); 1093 remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); 1094 1095 // Verify that the password needs rehashing. 1096 $hash = get_userdata( self::$user_id )->user_pass; 1097 $this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) ); 1098 1099 // Authenticate. 1100 $user = wp_authenticate( $username_or_email, $password ); 1101 1102 // Verify that the reduced cost password hash was valid. 1103 $this->assertNotWPError( $user ); 1104 $this->assertInstanceOf( 'WP_User', $user ); 1105 $this->assertSame( self::$user_id, $user->ID ); 1106 1107 // Verify that the password has been rehashed with the increased cost. 1108 $hash = get_userdata( self::$user_id )->user_pass; 1109 $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) ); 1110 $this->assertSame( self::get_default_bcrypt_cost(), password_get_info( substr( $hash, 3 ) )['options']['cost'] ); 1111 1112 // Authenticate a second time to ensure the new hash is valid. 1113 $user = wp_authenticate( $username_or_email, $password ); 1114 1115 // Verify that the password hash is valid. 1116 $this->assertNotWPError( $user ); 1117 $this->assertInstanceOf( 'WP_User', $user ); 1118 $this->assertSame( self::$user_id, $user->ID ); 1119 } 1120 1121 public function reduce_hash_cost( array $options ): array { 1122 $options['cost'] = self::get_default_bcrypt_cost() - 1; 1123 return $options; 1124 } 1125 1126 public function increase_hash_cost( array $options ): array { 1127 $options['cost'] = self::get_default_bcrypt_cost() + 1; 1128 return $options; 1129 } 1130 1131 public function data_usernames() { 1132 return array( 1133 array( 1134 self::USER_LOGIN, 1135 ), 1136 array( 1137 self::USER_EMAIL, 1138 ), 1139 ); 1140 } 1141 1142 /** 1143 * @ticket 21022 1144 */ 1145 public function test_password_rehashing_requirement_can_be_filtered() { 1146 $filter_count_before = did_filter( 'password_needs_rehash' ); 1147 1148 wp_password_needs_rehash( '$hash' ); 1149 1150 $this->assertSame( $filter_count_before + 1, did_filter( 'password_needs_rehash' ) ); 1151 } 1152 1153 /** 1154 * @ticket 21022 1155 */ 1156 public function test_password_hashing_algorithm_can_be_filtered() { 1157 $password = 'password'; 1158 1159 $filter_count_before = did_filter( 'wp_hash_password_algorithm' ); 1160 1161 $wp_hash = wp_hash_password( $password ); 1162 1163 wp_check_password( $password, $wp_hash ); 1164 wp_password_needs_rehash( $wp_hash ); 1165 1166 $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_algorithm' ) ); 1167 } 1168 1169 /** 1170 * @ticket 21022 1171 */ 1172 public function test_password_hashing_options_can_be_filtered() { 1173 $password = 'password'; 1174 1175 add_filter( 1176 'wp_hash_password_options', 1177 static function ( $options ) { 1178 $options['cost'] = 5; 1179 return $options; 1180 } 1181 ); 1182 1183 $filter_count_before = did_filter( 'wp_hash_password_options' ); 1184 1185 $wp_hash = wp_hash_password( $password ); 1186 $valid = wp_check_password( $password, $wp_hash ); 1187 $needs_rehash = wp_password_needs_rehash( $wp_hash ); 1188 $info = password_get_info( substr( $wp_hash, 3 ) ); 1189 $cost = $info['options']['cost']; 1190 1191 $this->assertTrue( $valid ); 1192 $this->assertFalse( $needs_rehash ); 1193 $this->assertSame( $filter_count_before + 2, did_filter( 'wp_hash_password_options' ) ); 1194 $this->assertSame( 5, $cost ); 1195 } 1196 1197 /** 1198 * @ticket 21022 1199 */ 1200 public function test_password_checks_support_wp_hasher_fallback() { 1201 global $wp_hasher; 1202 1203 $filter_count_before = did_filter( 'wp_hash_password_options' ); 1204 1205 $password = 'password'; 1206 1207 // Ensure the global $wp_hasher is set. 1208 $wp_hasher = new WP_Fake_Hasher(); 1209 1210 $hasher_hash = $wp_hasher->HashPassword( $password ); 1211 $wp_hash = wp_hash_password( $password ); 1212 $valid = wp_check_password( $password, $wp_hash ); 1213 $needs_rehash = wp_password_needs_rehash( $wp_hash ); 1214 1215 // Reset the global $wp_hasher. 1216 $wp_hasher = null; 1217 1218 $this->assertSame( $hasher_hash, $wp_hash ); 1219 $this->assertTrue( $valid ); 1220 $this->assertFalse( $needs_rehash ); 1221 $this->assertSame( 1, did_filter( 'check_password' ) ); 1222 $this->assertSame( $filter_count_before, did_filter( 'wp_hash_password_options' ) ); 458 1223 } 459 1224 … … 704 1469 */ 705 1470 public function test_authenticate_application_password_respects_existing_user() { 706 $this->assertSame( self::$_user, wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ) ); 1471 $user = wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ); 1472 $this->assertNotWPError( $user ); 1473 $this->assertSame( self::$_user, $user ); 707 1474 } 708 1475 … … 713 1480 add_filter( 'application_password_is_api_request', '__return_false' ); 714 1481 715 $this->assertNull( wp_authenticate_application_password( null, self::$_user->user_login, 'password' ) ); 1482 $user = wp_authenticate_application_password( null, self::$_user->user_login, 'password' ); 1483 $this->assertNotWPError( $user ); 1484 $this->assertNull( $user ); 716 1485 } 717 1486 … … 806 1575 807 1576 $user = wp_authenticate_application_password( null, self::$_user->user_login, $password ); 1577 $this->assertNotWPError( $user ); 808 1578 $this->assertInstanceOf( WP_User::class, $user ); 809 1579 $this->assertSame( self::$user_id, $user->ID ); … … 820 1590 821 1591 $user = wp_authenticate_application_password( null, self::$_user->user_email, $password ); 1592 $this->assertNotWPError( $user ); 822 1593 $this->assertInstanceOf( WP_User::class, $user ); 823 1594 $this->assertSame( self::$user_id, $user->ID ); … … 834 1605 835 1606 $user = wp_authenticate_application_password( null, self::$_user->user_email, WP_Application_Passwords::chunk_password( $password ) ); 1607 $this->assertNotWPError( $user ); 836 1608 $this->assertInstanceOf( WP_User::class, $user ); 837 1609 $this->assertSame( self::$user_id, $user->ID ); … … 845 1617 846 1618 $authenticated = wp_authenticate_application_password( null, 'idonotexist', 'password' ); 1619 $this->assertNotWPError( $authenticated ); 847 1620 $this->assertNull( $authenticated ); 848 1621 } … … 968 1741 $this->assertSame( $_SERVER['PHP_AUTH_PW'], 'pass:word' ); 969 1742 } 1743 1744 /** 1745 * Test the tests 1746 * 1747 * @covers Tests_Auth::set_user_password_with_phpass 1748 * 1749 * @ticket 21022 1750 */ 1751 public function test_set_user_password_with_phpass() { 1752 // Set the user password with the old phpass algorithm. 1753 self::set_user_password_with_phpass( 'password', self::$user_id ); 1754 1755 // Ensure the password is hashed with phpass. 1756 $hash = get_userdata( self::$user_id )->user_pass; 1757 $this->assertStringStartsWith( '$P$', $hash ); 1758 } 1759 1760 private static function set_user_password_with_phpass( string $password, int $user_id ) { 1761 global $wpdb; 1762 1763 $wpdb->update( 1764 $wpdb->users, 1765 array( 1766 'user_pass' => self::$wp_hasher->HashPassword( $password ), 1767 ), 1768 array( 1769 'ID' => $user_id, 1770 ) 1771 ); 1772 clean_user_cache( $user_id ); 1773 } 1774 1775 1776 /** 1777 * Test the tests 1778 * 1779 * @covers Tests_Auth::set_user_password_with_plain_bcrypt 1780 * 1781 * @ticket 21022 1782 */ 1783 public function test_set_user_password_with_plain_bcrypt() { 1784 // Set the user password with plain bcrypt. 1785 self::set_user_password_with_plain_bcrypt( 'password', self::$user_id ); 1786 1787 // Ensure the password is hashed with bcrypt. 1788 $hash = get_userdata( self::$user_id )->user_pass; 1789 $this->assertStringStartsWith( '$2y$', $hash ); 1790 } 1791 1792 private static function set_user_password_with_plain_bcrypt( string $password, int $user_id ) { 1793 global $wpdb; 1794 1795 $wpdb->update( 1796 $wpdb->users, 1797 array( 1798 'user_pass' => password_hash( 'password', PASSWORD_BCRYPT ), 1799 ), 1800 array( 1801 'ID' => $user_id, 1802 ) 1803 ); 1804 clean_user_cache( $user_id ); 1805 } 1806 1807 /** 1808 * Test the tests 1809 * 1810 * @covers Tests_Auth::set_application_password_with_phpass 1811 * 1812 * @ticket 21022 1813 */ 1814 public function test_set_application_password_with_phpass() { 1815 // Set an application password with the old phpass algorithm. 1816 $uuid = self::set_application_password_with_phpass( 'password', self::$user_id ); 1817 1818 // Ensure the password is hashed with phpass. 1819 $hash = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid )['password']; 1820 $this->assertStringStartsWith( '$P$', $hash ); 1821 } 1822 1823 private static function set_application_password_with_phpass( string $password, int $user_id ) { 1824 $uuid = wp_generate_uuid4(); 1825 $item = array( 1826 'uuid' => $uuid, 1827 'app_id' => '', 1828 'name' => 'Test', 1829 'password' => self::$wp_hasher->HashPassword( $password ), 1830 'created' => time(), 1831 'last_used' => null, 1832 'last_ip' => null, 1833 ); 1834 1835 $saved = update_user_meta( 1836 $user_id, 1837 WP_Application_Passwords::USERMETA_KEY_APPLICATION_PASSWORDS, 1838 array( $item ) 1839 ); 1840 1841 if ( ! $saved ) { 1842 throw new Exception( 'Could not save application password.' ); 1843 } 1844 1845 update_network_option( get_main_network_id(), WP_Application_Passwords::OPTION_KEY_IN_USE, true ); 1846 1847 return $uuid; 1848 } 1849 1850 private static function get_default_bcrypt_cost(): int { 1851 $hash = password_hash( 'password', PASSWORD_BCRYPT ); 1852 $info = password_get_info( $hash ); 1853 1854 return $info['options']['cost']; 1855 } 970 1856 }
Note: See TracChangeset
for help on using the changeset viewer.