@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 PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider {
4
5 private $adapter;
6
7 public function getProviderName() {
8 return pht('Username/Password');
9 }
10
11 public function getConfigurationHelp() {
12 return pht(
13 "(WARNING) Examine the table below for information on how password ".
14 "hashes will be stored in the database.\n\n".
15 "(NOTE) You can select a minimum password length by setting ".
16 "`%s` in configuration.",
17 'account.minimum-password-length');
18 }
19
20 public function renderConfigurationFooter() {
21 $hashers = PhabricatorPasswordHasher::getAllHashers();
22 $hashers = msort($hashers, 'getStrength');
23 $hashers = array_reverse($hashers);
24
25 $yes = phutil_tag(
26 'strong',
27 array(
28 'style' => 'color: #009900',
29 ),
30 pht('Yes'));
31
32 $no = phutil_tag(
33 'strong',
34 array(
35 'style' => 'color: #990000',
36 ),
37 pht('Not Installed'));
38
39 $best_hasher_name = null;
40 try {
41 $best_hasher = PhabricatorPasswordHasher::getBestHasher();
42 $best_hasher_name = $best_hasher->getHashName();
43 } catch (PhabricatorPasswordHasherUnavailableException $ex) {
44 // There are no suitable hashers. The user might be able to enable some,
45 // so we don't want to fatal here. We'll fatal when users try to actually
46 // use this stuff if it isn't fixed before then. Until then, we just
47 // don't highlight a row. In practice, at least one hasher should always
48 // be available.
49 }
50
51 $rows = array();
52 $rowc = array();
53 foreach ($hashers as $hasher) {
54 $is_installed = $hasher->canHashPasswords();
55
56 $rows[] = array(
57 $hasher->getHumanReadableName(),
58 $hasher->getHashName(),
59 $hasher->getHumanReadableStrength(),
60 ($is_installed ? $yes : $no),
61 ($is_installed ? null : $hasher->getInstallInstructions()),
62 );
63 $rowc[] = ($best_hasher_name == $hasher->getHashName())
64 ? 'highlighted'
65 : null;
66 }
67
68 $table = new AphrontTableView($rows);
69 $table->setRowClasses($rowc);
70 $table->setHeaders(
71 array(
72 pht('Algorithm'),
73 pht('Name'),
74 pht('Strength'),
75 pht('Installed'),
76 pht('Install Instructions'),
77 ));
78
79 $table->setColumnClasses(
80 array(
81 '',
82 '',
83 '',
84 '',
85 'wide',
86 ));
87
88 $header = id(new PHUIHeaderView())
89 ->setHeader(pht('Password Hash Algorithms'))
90 ->setSubheader(
91 pht(
92 'Stronger algorithms are listed first. The highlighted algorithm '.
93 'will be used when storing new hashes. Older hashes will be '.
94 'upgraded to the best algorithm over time.'));
95
96 return id(new PHUIObjectBoxView())
97 ->setHeader($header)
98 ->setTable($table);
99 }
100
101 public function getDescriptionForCreate() {
102 return pht(
103 'Allow users to log in or register using a username and password.');
104 }
105
106 public function getAdapter() {
107 if (!$this->adapter) {
108 $adapter = new PhutilEmptyAuthAdapter();
109 $adapter->setAdapterType('password');
110 $adapter->setAdapterDomain('self');
111 $this->adapter = $adapter;
112 }
113 return $this->adapter;
114 }
115
116 public function getLoginOrder() {
117 // Make sure username/password appears first if it is enabled.
118 return '100-'.$this->getProviderName();
119 }
120
121 public function shouldAllowAccountLink() {
122 return false;
123 }
124
125 public function shouldAllowAccountUnlink() {
126 return false;
127 }
128
129 public function isDefaultRegistrationProvider() {
130 return true;
131 }
132
133 public function buildLoginForm(
134 PhabricatorAuthStartController $controller) {
135 $request = $controller->getRequest();
136 return $this->renderPasswordLoginForm($request);
137 }
138
139 public function buildInviteForm(
140 PhabricatorAuthStartController $controller) {
141 $request = $controller->getRequest();
142 $viewer = $request->getViewer();
143
144 $form = id(new AphrontFormView())
145 ->setUser($viewer)
146 ->addHiddenInput('invite', true)
147 ->appendChild(
148 id(new AphrontFormTextControl())
149 ->setLabel(pht('Username'))
150 ->setName('username'));
151
152 $dialog = id(new AphrontDialogView())
153 ->setUser($viewer)
154 ->setTitle(pht('Register an Account'))
155 ->appendForm($form)
156 ->setSubmitURI('/auth/register/')
157 ->addSubmitButton(pht('Continue'));
158
159 return $dialog;
160 }
161
162 public function buildLinkForm($controller) {
163 throw new Exception(pht("Password providers can't be linked."));
164 }
165
166 private function renderPasswordLoginForm(
167 AphrontRequest $request,
168 $require_captcha = false,
169 $captcha_valid = false) {
170
171 $viewer = $request->getUser();
172
173 $dialog = id(new AphrontDialogView())
174 ->setSubmitURI($this->getLoginURI())
175 ->setUser($viewer)
176 ->setTitle(pht('Log In'))
177 ->addSubmitButton(pht('Log In'));
178
179 if ($this->shouldAllowRegistration()) {
180 $dialog->addCancelButton(
181 '/auth/register/',
182 pht('Register New Account'));
183 }
184
185 $dialog->addFooter(
186 phutil_tag(
187 'a',
188 array(
189 'href' => '/login/email/',
190 ),
191 pht('Forgot your password?')));
192
193 $v_user = nonempty(
194 $request->getStr('username'),
195 $request->getCookie(PhabricatorCookies::COOKIE_USERNAME));
196
197 $e_user = null;
198 $e_pass = null;
199 $e_captcha = null;
200
201 $errors = array();
202 if ($require_captcha && !$captcha_valid) {
203 if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) {
204 $e_captcha = pht('Invalid');
205 $errors[] = pht('CAPTCHA was not entered correctly.');
206 } else {
207 $e_captcha = pht('Required');
208 $errors[] = pht(
209 'Too many login failures recently. You must '.
210 'submit a CAPTCHA with your login request.');
211 }
212 } else if ($request->isHTTPPost()) {
213 // NOTE: This is intentionally vague so as not to disclose whether a
214 // given username or email is registered.
215 $e_user = pht('Invalid');
216 $e_pass = pht('Invalid');
217 $errors[] = pht('Username or password are incorrect.');
218 }
219
220 if ($errors) {
221 $errors = id(new PHUIInfoView())->setErrors($errors);
222 }
223
224 $form = id(new PHUIFormLayoutView())
225 ->setFullWidth(true)
226 ->appendChild($errors)
227 ->appendChild(
228 id(new AphrontFormTextControl())
229 ->setLabel(pht('Username or Email'))
230 ->setName('username')
231 ->setAutofocus(true)
232 ->setValue($v_user)
233 ->setError($e_user))
234 ->appendChild(
235 id(new AphrontFormPasswordControl())
236 ->setLabel(pht('Password'))
237 ->setName('password')
238 ->setError($e_pass));
239
240 if ($require_captcha) {
241 $form->appendChild(
242 id(new AphrontFormRecaptchaControl())
243 ->setError($e_captcha));
244 }
245
246 $dialog->appendChild($form);
247
248 return $dialog;
249 }
250
251 public function processLoginRequest(
252 PhabricatorAuthLoginController $controller) {
253
254 $request = $controller->getRequest();
255 $viewer = $request->getUser();
256 $content_source = PhabricatorContentSource::newFromRequest($request);
257
258 $rate_actor = PhabricatorSystemActionEngine::newActorFromRequest($request);
259
260 PhabricatorSystemActionEngine::willTakeAction(
261 array($rate_actor),
262 new PhabricatorAuthTryPasswordAction(),
263 1);
264
265 // If the same remote address has submitted several failed login attempts
266 // recently, require they provide a CAPTCHA response for new attempts.
267 $require_captcha = false;
268 $captcha_valid = false;
269 if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) {
270 try {
271 PhabricatorSystemActionEngine::willTakeAction(
272 array($rate_actor),
273 new PhabricatorAuthTryPasswordWithoutCAPTCHAAction(),
274 1);
275 } catch (PhabricatorSystemActionRateLimitException $ex) {
276 $require_captcha = true;
277 $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request);
278 }
279 }
280
281 $response = null;
282 $account = null;
283 $log_user = null;
284
285 if ($request->isFormPost()) {
286 if (!$require_captcha || $captcha_valid) {
287 $username_or_email = $request->getStr('username');
288 if (strlen($username_or_email)) {
289 $user = id(new PhabricatorUser())->loadOneWhere(
290 'username = %s',
291 $username_or_email);
292
293 if (!$user) {
294 $user = PhabricatorUser::loadOneWithEmailAddress(
295 $username_or_email);
296 }
297
298 if ($user) {
299 $envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
300
301 $engine = id(new PhabricatorAuthPasswordEngine())
302 ->setViewer($user)
303 ->setContentSource($content_source)
304 ->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT)
305 ->setObject($user);
306
307 if ($engine->isValidPassword($envelope)) {
308 $account = $this->newExternalAccountForUser($user);
309 $log_user = $user;
310 }
311 }
312 }
313 }
314 }
315
316 if (!$account) {
317 if ($request->isFormPost()) {
318 $log = PhabricatorUserLog::initializeNewLog(
319 null,
320 $log_user ? $log_user->getPHID() : null,
321 PhabricatorLoginFailureUserLogType::LOGTYPE);
322 $log->save();
323 }
324
325 $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
326
327 $response = $controller->buildProviderPageResponse(
328 $this,
329 $this->renderPasswordLoginForm(
330 $request,
331 $require_captcha,
332 $captcha_valid));
333 }
334
335 return array($account, $response);
336 }
337
338 public function shouldRequireRegistrationPassword() {
339 return true;
340 }
341
342 public static function getPasswordProvider() {
343 $providers = self::getAllEnabledProviders();
344
345 foreach ($providers as $provider) {
346 if ($provider instanceof PhabricatorPasswordAuthProvider) {
347 return $provider;
348 }
349 }
350
351 return null;
352 }
353
354 public function willRenderLinkedAccount(
355 PhabricatorUser $viewer,
356 PHUIObjectItemView $item,
357 PhabricatorExternalAccount $account) {
358 return;
359 }
360
361 public function shouldAllowAccountRefresh() {
362 return false;
363 }
364
365 public function shouldAllowEmailTrustConfiguration() {
366 return false;
367 }
368
369}