@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorAuthPasswordTestCase extends PhabricatorTestCase {
4
5 protected function getPhabricatorTestCaseConfiguration() {
6 return array(
7 self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
8 );
9 }
10
11 public function testCompare() {
12 $password1 = new PhutilOpaqueEnvelope('hunter2');
13 $password2 = new PhutilOpaqueEnvelope('hunter3');
14
15 $user = $this->generateNewTestUser();
16 $type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST;
17
18 $pass = PhabricatorAuthPassword::initializeNewPassword($user, $type)
19 ->setPassword($password1, $user)
20 ->save();
21
22 $this->assertTrue(
23 $pass->comparePassword($password1, $user),
24 pht('Good password should match.'));
25
26 $this->assertFalse(
27 $pass->comparePassword($password2, $user),
28 pht('Bad password should not match.'));
29 }
30
31 public function testPasswordEngine() {
32 $password1 = new PhutilOpaqueEnvelope('the quick');
33 $password2 = new PhutilOpaqueEnvelope('brown fox');
34
35 $user = $this->generateNewTestUser();
36 $test_type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST;
37 $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
38 $content_source = $this->newContentSource();
39
40 $engine = id(new PhabricatorAuthPasswordEngine())
41 ->setViewer($user)
42 ->setContentSource($content_source)
43 ->setPasswordType($test_type)
44 ->setObject($user);
45
46 $account_engine = id(new PhabricatorAuthPasswordEngine())
47 ->setViewer($user)
48 ->setContentSource($content_source)
49 ->setPasswordType($account_type)
50 ->setObject($user);
51
52 // We haven't set any passwords yet, so both passwords should be
53 // invalid.
54 $this->assertFalse($engine->isValidPassword($password1));
55 $this->assertFalse($engine->isValidPassword($password2));
56
57 $pass = PhabricatorAuthPassword::initializeNewPassword($user, $test_type)
58 ->setPassword($password1, $user)
59 ->save();
60
61 // The password should now be valid.
62 $this->assertTrue($engine->isValidPassword($password1));
63 $this->assertFalse($engine->isValidPassword($password2));
64
65 // But, since the password is a "test" password, it should not be a valid
66 // "account" password.
67 $this->assertFalse($account_engine->isValidPassword($password1));
68 $this->assertFalse($account_engine->isValidPassword($password2));
69
70 // Both passwords are unique for the "test" engine, since an active
71 // password of a given type doesn't collide with itself.
72 $this->assertTrue($engine->isUniquePassword($password1));
73 $this->assertTrue($engine->isUniquePassword($password2));
74
75 // The "test" password is no longer unique for the "account" engine.
76 $this->assertFalse($account_engine->isUniquePassword($password1));
77 $this->assertTrue($account_engine->isUniquePassword($password2));
78
79 $this->revokePassword($user, $pass);
80
81 // Now that we've revoked the password, it should no longer be valid.
82 $this->assertFalse($engine->isValidPassword($password1));
83 $this->assertFalse($engine->isValidPassword($password2));
84
85 // But it should be a revoked password.
86 $this->assertTrue($engine->isRevokedPassword($password1));
87 $this->assertFalse($engine->isRevokedPassword($password2));
88
89 // It should be revoked for both roles: revoking a "test" password also
90 // prevents you from choosing it as a new "account" password.
91 $this->assertTrue($account_engine->isRevokedPassword($password1));
92 $this->assertFalse($account_engine->isValidPassword($password2));
93
94 // The revoked password makes this password non-unique for all account
95 // types.
96 $this->assertFalse($engine->isUniquePassword($password1));
97 $this->assertTrue($engine->isUniquePassword($password2));
98 $this->assertFalse($account_engine->isUniquePassword($password1));
99 $this->assertTrue($account_engine->isUniquePassword($password2));
100 }
101
102 public function testPasswordBlocklisting() {
103 $user = $this->generateNewTestUser();
104
105 $user
106 ->setUsername('iasimov')
107 ->setRealName('Isaac Asimov')
108 ->save();
109
110 $test_type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST;
111 $content_source = $this->newContentSource();
112
113 $engine = id(new PhabricatorAuthPasswordEngine())
114 ->setViewer($user)
115 ->setContentSource($content_source)
116 ->setPasswordType($test_type)
117 ->setObject($user);
118
119 $env = PhabricatorEnv::beginScopedEnv();
120 $env->overrideEnvConfig('account.minimum-password-length', 4);
121
122 $passwords = array(
123 'a23li432m9mdf' => true,
124
125 // Empty.
126 '' => false,
127
128 // Password length tests.
129 'xh3' => false,
130 'xh32' => true,
131
132 // In common password blocklist.
133 'password1' => false,
134
135 // Tests for the account identifier blocklist.
136 'isaac' => false,
137 'iasimov' => false,
138 'iasimov1' => false,
139 'asimov' => false,
140 'iSaAc' => false,
141 '32IASIMOV' => false,
142 'i-am-iasimov-this-is-my-long-strong-password' => false,
143 'iasimo' => false,
144
145 // These are okay: although they're visually similar, they aren't mutual
146 // substrings of any identifier.
147 'iasimo1' => true,
148 'isa1mov' => true,
149 );
150
151 foreach ($passwords as $password => $expect) {
152 $this->assertBlocklistedPassword($engine, $password, $expect);
153 }
154 }
155
156 private function assertBlocklistedPassword(
157 PhabricatorAuthPasswordEngine $engine,
158 $raw_password,
159 $expect_valid) {
160
161 $envelope_1 = new PhutilOpaqueEnvelope($raw_password);
162 $envelope_2 = new PhutilOpaqueEnvelope($raw_password);
163
164 $caught = null;
165 try {
166 $engine->checkNewPassword($envelope_1, $envelope_2);
167 } catch (PhabricatorAuthPasswordException $exception) {
168 $caught = $exception;
169 }
170
171 $this->assertEqual(
172 $expect_valid,
173 !($caught instanceof PhabricatorAuthPasswordException),
174 pht('Validity of password "%s".', $raw_password));
175 }
176
177
178 public function testPasswordUpgrade() {
179 $weak_hasher = new PhabricatorIteratedMD5PasswordHasher();
180
181 // Make sure we have two different hashers, and that the second one is
182 // stronger than iterated MD5. The most common reason this would fail is
183 // if an install does not have bcrypt available.
184 $strong_hasher = PhabricatorPasswordHasher::getBestHasher();
185 if ($strong_hasher->getStrength() <= $weak_hasher->getStrength()) {
186 $this->assertSkipped(
187 pht(
188 'Multiple password hashers of different strengths are not '.
189 'available, so hash upgrading can not be tested.'));
190 }
191
192 $envelope = new PhutilOpaqueEnvelope('lunar1997');
193
194 $user = $this->generateNewTestUser();
195 $type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST;
196 $content_source = $this->newContentSource();
197
198 $engine = id(new PhabricatorAuthPasswordEngine())
199 ->setViewer($user)
200 ->setContentSource($content_source)
201 ->setPasswordType($type)
202 ->setObject($user);
203
204 $password = PhabricatorAuthPassword::initializeNewPassword($user, $type)
205 ->setPasswordWithHasher($envelope, $user, $weak_hasher)
206 ->save();
207
208 $weak_name = $weak_hasher->getHashName();
209 $strong_name = $strong_hasher->getHashName();
210
211 // Since we explicitly used the weak hasher, the password should have
212 // been hashed with it.
213 $actual_hasher = $password->getHasher();
214 $this->assertEqual($weak_name, $actual_hasher->getHashName());
215
216 $is_valid = $engine
217 ->setUpgradeHashers(false)
218 ->isValidPassword($envelope, $user);
219 $password->reload();
220
221 // Since we disabled hasher upgrading, the password should not have been
222 // rehashed.
223 $this->assertTrue($is_valid);
224 $actual_hasher = $password->getHasher();
225 $this->assertEqual($weak_name, $actual_hasher->getHashName());
226
227 $is_valid = $engine
228 ->setUpgradeHashers(true)
229 ->isValidPassword($envelope, $user);
230 $password->reload();
231
232 // Now that we enabled hasher upgrading, the password should have been
233 // automatically rehashed into the stronger format.
234 $this->assertTrue($is_valid);
235 $actual_hasher = $password->getHasher();
236 $this->assertEqual($strong_name, $actual_hasher->getHashName());
237
238 // We should also have an "upgrade" transaction in the transaction record
239 // now which records the two hasher names.
240 $xactions = id(new PhabricatorAuthPasswordTransactionQuery())
241 ->setViewer($user)
242 ->withObjectPHIDs(array($password->getPHID()))
243 ->withTransactionTypes(
244 array(
245 PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE,
246 ))
247 ->execute();
248
249 $this->assertEqual(1, count($xactions));
250 $xaction = head($xactions);
251
252 $this->assertEqual($weak_name, $xaction->getOldValue());
253 $this->assertEqual($strong_name, $xaction->getNewValue());
254
255 $is_valid = $engine
256 ->isValidPassword($envelope, $user);
257
258 // Finally, the password should still be valid after all the dust has
259 // settled.
260 $this->assertTrue($is_valid);
261 }
262
263 private function revokePassword(
264 PhabricatorUser $actor,
265 PhabricatorAuthPassword $password) {
266
267 $content_source = $this->newContentSource();
268 $revoke_type = PhabricatorAuthPasswordRevokeTransaction::TRANSACTIONTYPE;
269
270 $xactions = array();
271
272 $xactions[] = $password->getApplicationTransactionTemplate()
273 ->setTransactionType($revoke_type)
274 ->setNewValue(true);
275
276 $editor = $password->getApplicationTransactionEditor()
277 ->setActor($actor)
278 ->setContinueOnNoEffect(true)
279 ->setContinueOnMissingFields(true)
280 ->setContentSource($content_source)
281 ->applyTransactions($password, $xactions);
282 }
283
284}