PageRenderTime 47ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/core/modules/user/src/Tests/UserPasswordResetTest.php

http://github.com/drupal/drupal
PHP | 273 lines | 152 code | 44 blank | 77 comment | 0 complexity | 9f9c2aed59f104437640c8bbb7cbc9bf MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\user\Tests;
  3. use Drupal\system\Tests\Cache\PageCacheTagsTestBase;
  4. use Drupal\user\Entity\User;
  5. /**
  6. * Ensure that password reset methods work as expected.
  7. *
  8. * @group user
  9. */
  10. class UserPasswordResetTest extends PageCacheTagsTestBase {
  11. /**
  12. * The profile to install as a basis for testing.
  13. *
  14. * This test uses the standard profile to test the password reset in
  15. * combination with an ajax request provided by the user picture configuration
  16. * in the standard profile.
  17. *
  18. * @var string
  19. */
  20. protected $profile = 'standard';
  21. /**
  22. * The user object to test password resetting.
  23. *
  24. * @var \Drupal\user\UserInterface
  25. */
  26. protected $account;
  27. /**
  28. * Modules to enable.
  29. *
  30. * @var array
  31. */
  32. public static $modules = ['block'];
  33. /**
  34. * {@inheritdoc}
  35. */
  36. protected function setUp() {
  37. parent::setUp();
  38. $this->drupalPlaceBlock('system_menu_block:account');
  39. // Create a user.
  40. $account = $this->drupalCreateUser();
  41. // Activate user by logging in.
  42. $this->drupalLogin($account);
  43. $this->account = User::load($account->id());
  44. $this->account->pass_raw = $account->pass_raw;
  45. $this->drupalLogout();
  46. // Set the last login time that is used to generate the one-time link so
  47. // that it is definitely over a second ago.
  48. $account->login = REQUEST_TIME - mt_rand(10, 100000);
  49. db_update('users_field_data')
  50. ->fields(array('login' => $account->getLastLoginTime()))
  51. ->condition('uid', $account->id())
  52. ->execute();
  53. }
  54. /**
  55. * Tests password reset functionality.
  56. */
  57. function testUserPasswordReset() {
  58. // Try to reset the password for an invalid account.
  59. $this->drupalGet('user/password');
  60. $edit = array('name' => $this->randomMachineName(32));
  61. $this->drupalPostForm(NULL, $edit, t('Submit'));
  62. $this->assertText(t('@name is not recognized as a username or an email address.', array('@name' => $edit['name'])), 'Validation error message shown when trying to request password for invalid account.');
  63. $this->assertEqual(count($this->drupalGetMails(array('id' => 'user_password_reset'))), 0, 'No email was sent when requesting a password for an invalid account.');
  64. // Reset the password by username via the password reset page.
  65. $edit['name'] = $this->account->getUsername();
  66. $this->drupalPostForm(NULL, $edit, t('Submit'));
  67. // Verify that the user was sent an email.
  68. $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.');
  69. $subject = t('Replacement login information for @username at @site', array('@username' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')));
  70. $this->assertMail('subject', $subject, 'Password reset email subject is correct.');
  71. $resetURL = $this->getResetURL();
  72. $this->drupalGet($resetURL);
  73. $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
  74. // Ensure the password reset URL is not cached.
  75. $this->drupalGet($resetURL);
  76. $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
  77. // Check the one-time login page.
  78. $this->assertText($this->account->getUsername(), 'One-time login page contains the correct username.');
  79. $this->assertText(t('This login can be used only once.'), 'Found warning about one-time login.');
  80. $this->assertTitle(t('Reset password | Drupal'), 'Page title is "Reset password".');
  81. // Check successful login.
  82. $this->drupalPostForm(NULL, NULL, t('Log in'));
  83. $this->assertLink(t('Log out'));
  84. $this->assertTitle(t('@name | @site', array('@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name'))), 'Logged in using password reset link.');
  85. // Make sure the ajax request from uploading a user picture does not
  86. // invalidate the reset token.
  87. $image = current($this->drupalGetTestFiles('image'));
  88. $edit = array(
  89. 'files[user_picture_0]' => drupal_realpath($image->uri),
  90. );
  91. $this->drupalPostAjaxForm(NULL, $edit, 'user_picture_0_upload_button');
  92. // Change the forgotten password.
  93. $password = user_password();
  94. $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password);
  95. $this->drupalPostForm(NULL, $edit, t('Save'));
  96. $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');
  97. // Verify that the password reset session has been destroyed.
  98. $this->drupalPostForm(NULL, $edit, t('Save'));
  99. $this->assertText(t('Your current password is missing or incorrect; it\'s required to change the Password.'), 'Password needed to make profile changes.');
  100. // Log out, and try to log in again using the same one-time link.
  101. $this->drupalLogout();
  102. $this->drupalGet($resetURL);
  103. $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.');
  104. // Request a new password again, this time using the email address.
  105. $this->drupalGet('user/password');
  106. // Count email messages before to compare with after.
  107. $before = count($this->drupalGetMails(array('id' => 'user_password_reset')));
  108. $edit = array('name' => $this->account->getEmail());
  109. $this->drupalPostForm(NULL, $edit, t('Submit'));
  110. $this->assertTrue( count($this->drupalGetMails(array('id' => 'user_password_reset'))) === $before + 1, 'Email sent when requesting password reset using email address.');
  111. // Visit the user edit page without pass-reset-token and make sure it does
  112. // not cause an error.
  113. $resetURL = $this->getResetURL();
  114. $this->drupalGet($resetURL);
  115. $this->drupalPostForm(NULL, NULL, t('Log in'));
  116. $this->drupalGet('user/' . $this->account->id() . '/edit');
  117. $this->assertNoText('Expected user_string to be a string, NULL given');
  118. $this->drupalLogout();
  119. // Create a password reset link as if the request time was 60 seconds older than the allowed limit.
  120. $timeout = $this->config('user.settings')->get('password_reset_timeout');
  121. $bogus_timestamp = REQUEST_TIME - $timeout - 60;
  122. $_uid = $this->account->id();
  123. $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp));
  124. $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.');
  125. // Create a user, block the account, and verify that a login link is denied.
  126. $timestamp = REQUEST_TIME - 1;
  127. $blocked_account = $this->drupalCreateUser()->block();
  128. $blocked_account->save();
  129. $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp));
  130. $this->assertResponse(403);
  131. // Verify a blocked user can not request a new password.
  132. $this->drupalGet('user/password');
  133. // Count email messages before to compare with after.
  134. $before = count($this->drupalGetMails(array('id' => 'user_password_reset')));
  135. $edit = array('name' => $blocked_account->getUsername());
  136. $this->drupalPostForm(NULL, $edit, t('Submit'));
  137. $this->assertRaw(t('%name is blocked or has not been activated yet.', array('%name' => $blocked_account->getUsername())), 'Notified user blocked accounts can not request a new password');
  138. $this->assertTrue(count($this->drupalGetMails(array('id' => 'user_password_reset'))) === $before, 'No email was sent when requesting password reset for a blocked account');
  139. // Verify a password reset link is invalidated when the user's email address changes.
  140. $this->drupalGet('user/password');
  141. $edit = array('name' => $this->account->getUsername());
  142. $this->drupalPostForm(NULL, $edit, t('Submit'));
  143. $old_email_reset_link = $this->getResetURL();
  144. $this->account->setEmail("1" . $this->account->getEmail());
  145. $this->account->save();
  146. $this->drupalGet($old_email_reset_link);
  147. $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.');
  148. }
  149. /**
  150. * Retrieves password reset email and extracts the login link.
  151. */
  152. public function getResetURL() {
  153. // Assume the most recent email.
  154. $_emails = $this->drupalGetMails();
  155. $email = end($_emails);
  156. $urls = array();
  157. preg_match('#.+user/reset/.+#', $email['body'], $urls);
  158. return $urls[0];
  159. }
  160. /**
  161. * Test user password reset while logged in.
  162. */
  163. public function testUserPasswordResetLoggedIn() {
  164. // Log in.
  165. $this->drupalLogin($this->account);
  166. // Reset the password by username via the password reset page.
  167. $this->drupalGet('user/password');
  168. $this->drupalPostForm(NULL, NULL, t('Submit'));
  169. // Click the reset URL while logged and change our password.
  170. $resetURL = $this->getResetURL();
  171. $this->drupalGet($resetURL);
  172. $this->drupalPostForm(NULL, NULL, t('Log in'));
  173. // Change the password.
  174. $password = user_password();
  175. $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password);
  176. $this->drupalPostForm(NULL, $edit, t('Save'));
  177. $this->assertText(t('The changes have been saved.'), 'Password changed.');
  178. }
  179. /**
  180. * Prefill the text box on incorrect login via link to password reset page.
  181. */
  182. public function testUserResetPasswordTextboxFilled() {
  183. $this->drupalGet('user/login');
  184. $edit = array(
  185. 'name' => $this->randomMachineName(),
  186. 'pass' => $this->randomMachineName(),
  187. );
  188. $this->drupalPostForm('user/login', $edit, t('Log in'));
  189. $this->assertRaw(t('Unrecognized username or password. <a href=":password">Forgot your password?</a>',
  190. array(':password' => \Drupal::url('user.pass', [], array('query' => array('name' => $edit['name']))))));
  191. unset($edit['pass']);
  192. $this->drupalGet('user/password', array('query' => array('name' => $edit['name'])));
  193. $this->assertFieldByName('name', $edit['name'], 'User name found.');
  194. }
  195. /**
  196. * Make sure that users cannot forge password reset URLs of other users.
  197. */
  198. function testResetImpersonation() {
  199. // Create two identical user accounts except for the user name. They must
  200. // have the same empty password, so we can't use $this->drupalCreateUser().
  201. $edit = array();
  202. $edit['name'] = $this->randomMachineName();
  203. $edit['mail'] = $edit['name'] . '@example.com';
  204. $edit['status'] = 1;
  205. $user1 = User::create($edit);
  206. $user1->save();
  207. $edit['name'] = $this->randomMachineName();
  208. $user2 = User::create($edit);
  209. $user2->save();
  210. // Unique password hashes are automatically generated, the only way to
  211. // change that is to update it directly in the database.
  212. db_update('users_field_data')
  213. ->fields(['pass' => NULL])
  214. ->condition('uid', [$user1->id(), $user2->id()], 'IN')
  215. ->execute();
  216. \Drupal::entityManager()->getStorage('user')->resetCache();
  217. $user1 = User::load($user1->id());
  218. $user2 = User::load($user2->id());
  219. $this->assertEqual($user1->getPassword(), $user2->getPassword(), 'Both users have the same password hash.');
  220. // The password reset URL must not be valid for the second user when only
  221. // the user ID is changed in the URL.
  222. $reset_url = user_pass_reset_url($user1);
  223. $attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url);
  224. $this->drupalGet($attack_reset_url);
  225. $this->assertNoText($user2->getUsername(), 'The invalid password reset page does not show the user name.');
  226. $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
  227. $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
  228. }
  229. }