UserPasswordResetTest.php 9.34 KB
Newer Older
1
2
3
4
5
6
7
8
9
<?php

/**
 * @file
 * Definition of Drupal\user\Tests\UserPasswordResetTest.
 */

namespace Drupal\user\Tests;

10
use Drupal\system\Tests\Cache\PageCacheTagsTestBase;
11
use Drupal\user\Entity\User;
12
13

/**
14
15
16
 * Ensure that password reset methods work as expected.
 *
 * @group user
17
 */
18
class UserPasswordResetTest extends PageCacheTagsTestBase {
19
20
21
22
23
24
25
26
27
28
29
30

  /**
   * The profile to install as a basis for testing.
   *
   * This test uses the standard profile to test the password reset in
   * combination with an ajax request provided by the user picture configuration
   * in the standard profile.
   *
   * @var string
   */
  protected $profile = 'standard';

31
32
33
  /**
   * The user object to test password resetting.
   *
34
   * @var \Drupal\user\UserInterface
35
36
   */
  protected $account;
37

38
39
40
41
42
43
44
45
46
47
  /**
   * Modules to enable.
   *
   * @var array
   */
  public static $modules = ['block'];

  /**
   * {@inheritdoc}
   */
48
  protected function setUp() {
49
50
    parent::setUp();

51
52
    $this->drupalPlaceBlock('system_menu_block:account');

53
54
    // Create a user.
    $account = $this->drupalCreateUser();
55
56

    // Activate user by logging in.
57
    $this->drupalLogin($account);
58

59
    $this->account = user_load($account->id());
60
    $this->drupalLogout();
61
62
63
64

    // Set the last login time that is used to generate the one-time link so
    // that it is definitely over a second ago.
    $account->login = REQUEST_TIME - mt_rand(10, 100000);
65
    db_update('users_field_data')
66
      ->fields(array('login' => $account->getLastLoginTime()))
67
      ->condition('uid', $account->id())
68
      ->execute();
69
70
71
  }

  /**
72
   * Tests password reset functionality.
73
   */
74
75
76
77
  function testUserPasswordReset() {
    // Try to reset the password for an invalid account.
    $this->drupalGet('user/password');

78
    $edit = array('name' => $this->randomMachineName(32));
79
    $this->drupalPostForm(NULL, $edit, t('Submit'));
80

81
82
    $this->assertText(t('Sorry, @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.');
    $this->assertEqual(count($this->drupalGetMails(array('id' => 'user_password_reset'))), 0, 'No email was sent when requesting a password for an invalid account.');
83
84

    // Reset the password by username via the password reset page.
85
    $edit['name'] = $this->account->getUsername();
86
    $this->drupalPostForm(NULL, $edit, t('Submit'));
87

88
89
     // Verify that the user was sent an email.
    $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.');
90
    $subject = t('Replacement login information for @username at @site', array('@username' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')));
91
    $this->assertMail('subject', $subject, 'Password reset email subject is correct.');
92
93
94

    $resetURL = $this->getResetURL();
    $this->drupalGet($resetURL);
95
96
97
98
99
    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));

    // Ensure the password reset URL is not cached.
    $this->drupalGet($resetURL);
    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
100
101

    // Check the one-time login page.
102
    $this->assertText($this->account->getUsername(), 'One-time login page contains the correct username.');
103
    $this->assertText(t('This login can be used only once.'), 'Found warning about one-time login.');
104
    $this->assertTitle(t('Reset password | Drupal'), 'Page title is "Reset password".');
105
106

    // Check successful login.
107
    $this->drupalPostForm(NULL, NULL, t('Log in'));
108
    $this->assertLink(t('Log out'));
109
    $this->assertTitle(t('@name | @site', array('@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name'))), 'Logged in using password reset link.');
110

111
112
113
114
115
116
117
118
    // Make sure the ajax request from uploading a user picture does not
    // invalidate the reset token.
    $image = current($this->drupalGetTestFiles('image'));
    $edit = array(
      'files[user_picture_0]' => drupal_realpath($image->uri),
    );
    $this->drupalPostAjaxForm(NULL, $edit, 'user_picture_0_upload_button');

119
120
121
122
123
124
125
126
127
128
    // Change the forgotten password.
    $password = user_password();
    $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password);
    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');

    // Verify that the password reset session has been destroyed.
    $this->drupalPostForm(NULL, $edit, t('Save'));
    $this->assertText(t('Your current password is missing or incorrect; it\'s required to change the Password.'), 'Password needed to make profile changes.');

129
    // Log out, and try to log in again using the same one-time link.
130
    $this->drupalLogout();
131
132
133
    $this->drupalGet($resetURL);
    $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.');

134
    // Request a new password again, this time using the email address.
135
136
137
    $this->drupalGet('user/password');
    // Count email messages before to compare with after.
    $before = count($this->drupalGetMails(array('id' => 'user_password_reset')));
138
    $edit = array('name' => $this->account->getEmail());
139
    $this->drupalPostForm(NULL, $edit, t('Submit'));
140
    $this->assertTrue( count($this->drupalGetMails(array('id' => 'user_password_reset'))) === $before + 1, 'Email sent when requesting password reset using email address.');
141

142
    // Create a password reset link as if the request time was 60 seconds older than the allowed limit.
143
    $timeout = $this->config('user.settings')->get('password_reset_timeout');
144
    $bogus_timestamp = REQUEST_TIME - $timeout - 60;
145
    $_uid = $this->account->id();
146
    $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account->getPassword(), $bogus_timestamp, $this->account->getLastLoginTime(), $this->account->id()));
147
    $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.');
148
149
150
151
152

    // Create a user, block the account, and verify that a login link is denied.
    $timestamp = REQUEST_TIME - 1;
    $blocked_account = $this->drupalCreateUser()->block();
    $blocked_account->save();
153
    $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account->getPassword(), $timestamp, $blocked_account->getLastLoginTime(), $this->account->id()));
154
    $this->assertResponse(403);
155
  }
156
157

  /**
158
   * Retrieves password reset email and extracts the login link.
159
160
161
162
163
164
165
166
167
168
   */
  public function getResetURL() {
    // Assume the most recent email.
    $_emails = $this->drupalGetMails();
    $email = end($_emails);
    $urls = array();
    preg_match('#.+user/reset/.+#', $email['body'], $urls);

    return $urls[0];
  }
169

170
171
172
  /**
   * Prefill the text box on incorrect login via link to password reset page.
   */
173
174
175
  public function testUserResetPasswordTextboxFilled() {
    $this->drupalGet('user/login');
    $edit = array(
176
177
      'name' => $this->randomMachineName(),
      'pass' => $this->randomMachineName(),
178
    );
179
    $this->drupalPostForm('user/login', $edit, t('Log in'));
180
    $this->assertRaw(t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>',
181
      array('@password' => \Drupal::url('user.pass', [], array('query' => array('name' => $edit['name']))))));
182
183
184
185
    unset($edit['pass']);
    $this->drupalGet('user/password', array('query' => array('name' => $edit['name'])));
    $this->assertFieldByName('name', $edit['name'], 'User name found.');
  }
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225

  /**
   * Make sure that users cannot forge password reset URLs of other users.
   */
  function testResetImpersonation() {
    // Create two identical user accounts except for the user name. They must
    // have the same empty password, so we can't use $this->drupalCreateUser().
    $edit = array();
    $edit['name'] = $this->randomMachineName();
    $edit['mail'] = $edit['name'] . '@example.com';
    $edit['status'] = 1;
    $user1 = User::create($edit);
    $user1->save();

    $edit['name'] = $this->randomMachineName();
    $user2 = User::create($edit);
    $user2->save();

    // Unique password hashes are automatically generated, the only way to
    // change that is to update it directly in the database.
    db_update('users_field_data')
      ->fields(['pass' => NULL])
      ->condition('uid', [$user1->id(), $user2->id()], 'IN')
      ->execute();
    \Drupal::entityManager()->getStorage('user')->resetCache();
    $user1 = User::load($user1->id());
    $user2 = User::load($user2->id());

    $this->assertEqual($user1->getPassword(), $user2->getPassword(), 'Both users have the same password hash.');

    // The password reset URL must not be valid for the second user when only
    // the user ID is changed in the URL.
    $reset_url = user_pass_reset_url($user1);
    $attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url);
    $this->drupalGet($attack_reset_url);
    $this->assertNoText($user2->getUsername(), 'The invalid password reset page does not show the user name.');
    $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
    $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.');
   }

226
}