3 final class PhabricatorMultiFactorSettingsPanel
4 extends PhabricatorSettingsPanel
{
8 public function getPanelKey() {
12 public function getPanelName() {
13 return pht('Multi-Factor Auth');
16 public function getPanelMenuIcon() {
20 public function getPanelGroupKey() {
21 return PhabricatorSettingsAuthenticationPanelGroup
::PANELGROUPKEY
;
24 public function isMultiFactorEnrollmentPanel() {
28 public function setIsEnrollment($is_enrollment) {
29 $this->isEnrollment
= $is_enrollment;
33 public function getIsEnrollment() {
34 return $this->isEnrollment
;
37 public function processRequest(AphrontRequest
$request) {
38 if ($request->getExists('new') ||
$request->getExists('providerPHID')) {
39 return $this->processNew($request);
42 if ($request->getExists('edit')) {
43 return $this->processEdit($request);
46 if ($request->getExists('delete')) {
47 return $this->processDelete($request);
50 $user = $this->getUser();
51 $viewer = $request->getUser();
53 $factors = id(new PhabricatorAuthFactorConfigQuery())
55 ->withUserPHIDs(array($user->getPHID()))
57 $factors = msortv($factors, 'newSortVector');
62 $highlight_id = $request->getInt('id');
63 foreach ($factors as $factor) {
64 $provider = $factor->getFactorProvider();
66 if ($factor->getID() == $highlight_id) {
67 $rowc[] = 'highlighted';
72 $status = $provider->newStatus();
73 $status_icon = $status->getFactorIcon();
74 $status_color = $status->getFactorColor();
76 $icon = id(new PHUIIconView())
77 ->setIcon("{$status_icon} {$status_color}")
78 ->setTooltip(pht('Provider: %s', $status->getName()));
80 $details = $provider->getConfigurationListDetails($factor, $viewer);
87 'href' => $this->getPanelURI('?edit='.$factor->getID()),
88 'sigil' => 'workflow',
90 $factor->getFactorName()),
91 $provider->getFactor()->getFactorShortName(),
92 $provider->getDisplayName(),
94 phabricator_datetime($factor->getDateCreated(), $viewer),
98 'href' => $this->getPanelURI('?delete='.$factor->getID()),
99 'sigil' => 'workflow',
100 'class' => 'small button button-grey',
106 $table = new AphrontTableView($rows);
107 $table->setNoDataString(
108 pht("You haven't added any authentication factors to your account yet."));
119 $table->setColumnClasses(
129 $table->setRowClasses($rowc);
130 $table->setDeviceVisibility(
141 $help_uri = PhabricatorEnv
::getDoclink(
142 'User Guide: Multi-Factor Authentication');
146 // If we're enrolling a new account in MFA, provide a small visual hint
147 // that this is the button they want to click.
148 if ($this->getIsEnrollment()) {
149 $add_color = PHUIButtonView
::BLUE
;
151 $add_color = PHUIButtonView
::GREY
;
154 $can_add = (bool)$this->loadActiveMFAProviders();
156 $buttons[] = id(new PHUIButtonView())
159 ->setText(pht('Add Auth Factor'))
160 ->setHref($this->getPanelURI('?new=true'))
162 ->setDisabled(!$can_add)
163 ->setColor($add_color);
165 $buttons[] = id(new PHUIButtonView())
168 ->setText(pht('Help'))
170 ->setColor(PHUIButtonView
::GREY
);
172 return $this->newBox(pht('Authentication Factors'), $table, $buttons);
175 private function processNew(AphrontRequest
$request) {
176 $viewer = $request->getUser();
177 $user = $this->getUser();
179 $cancel_uri = $this->getPanelURI();
181 // Check that we have providers before we send the user through the MFA
182 // gate, so you don't authenticate and then immediately get roadblocked.
183 $providers = $this->loadActiveMFAProviders();
186 return $this->newDialog()
187 ->setTitle(pht('No MFA Providers'))
190 'This install does not have any active MFA providers configured. '.
191 'At least one provider must be configured and active before you '.
192 'can add new MFA factors.'))
193 ->addCancelButton($cancel_uri);
196 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
201 $selected_phid = $request->getStr('providerPHID');
202 if (empty($providers[$selected_phid])) {
203 $selected_provider = null;
205 $selected_provider = $providers[$selected_phid];
207 // Only let the user continue creating a factor for a given provider if
208 // they actually pass the provider's checks.
209 if (!$selected_provider->canCreateNewConfiguration($viewer)) {
210 $selected_provider = null;
214 if (!$selected_provider) {
215 $menu = id(new PHUIObjectItemListView())
220 foreach ($providers as $provider_phid => $provider) {
221 $provider_uri = id(new PhutilURI($this->getPanelURI()))
222 ->replaceQueryParam('providerPHID', $provider_phid);
224 $is_enabled = $provider->canCreateNewConfiguration($viewer);
226 $item = id(new PHUIObjectItemView())
227 ->setHeader($provider->getDisplayName())
228 ->setImageIcon($provider->newIconView())
229 ->addAttribute($provider->getDisplayDescription());
233 ->setHref($provider_uri)
234 ->setClickable(true);
236 $item->setDisabled(true);
239 $create_description = $provider->getConfigurationCreateDescription(
241 if ($create_description) {
242 $item->appendChild($create_description);
245 $menu->addItem($item);
248 return $this->newDialog()
249 ->setTitle(pht('Choose Factor Type'))
251 ->addCancelButton($cancel_uri);
254 // NOTE: Beyond providing guidance, this step is also providing a CSRF gate
255 // on this endpoint, since prompting the user to respond to a challenge
256 // sometimes requires us to push a challenge to them as a side effect (for
257 // example, with SMS).
258 if (!$request->isFormPost() ||
!$request->getBool('mfa.start')) {
259 $enroll = $selected_provider->getEnrollMessage();
260 if (!strlen($enroll)) {
261 $enroll = $selected_provider->getEnrollDescription($viewer);
264 return $this->newDialog()
265 ->addHiddenInput('providerPHID', $selected_provider->getPHID())
266 ->addHiddenInput('mfa.start', 1)
267 ->setTitle(pht('Add Authentication Factor'))
268 ->appendChild(new PHUIRemarkupView($viewer, $enroll))
269 ->addCancelButton($cancel_uri)
270 ->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
273 $form = id(new AphrontFormView())
274 ->setViewer($viewer);
276 if ($request->getBool('mfa.enroll')) {
277 // Subject users to rate limiting so that it's difficult to add factors
278 // by pure brute force. This is normally not much of an attack, but push
279 // factor types may have side effects.
280 PhabricatorSystemActionEngine
::willTakeAction(
281 array($viewer->getPHID()),
282 new PhabricatorAuthNewFactorAction(),
285 // Test the limit before showing the user a form, so we don't give them
286 // a form which can never possibly work because it will always hit rate
288 PhabricatorSystemActionEngine
::willTakeAction(
289 array($viewer->getPHID()),
290 new PhabricatorAuthNewFactorAction(),
294 $config = $selected_provider->processAddFactorForm(
300 // If the user added a factor, give them a rate limiting point back.
301 PhabricatorSystemActionEngine
::willTakeAction(
302 array($viewer->getPHID()),
303 new PhabricatorAuthNewFactorAction(),
308 // If we used a temporary token to handle synchronizing the factor,
310 $sync_token = $config->getMFASyncToken();
312 $sync_token->revokeToken();
315 $log = PhabricatorUserLog
::initializeNewLog(
318 PhabricatorAddMultifactorUserLogType
::LOGTYPE
);
321 $user->updateMultiFactorEnrollment();
323 // Terminate other sessions so they must log in and survive the
324 // multi-factor auth check.
326 id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
328 new PhutilOpaqueEnvelope(
329 $request->getCookie(PhabricatorCookies
::COOKIE_SESSION
)));
331 return id(new AphrontRedirectResponse())
332 ->setURI($this->getPanelURI('?id='.$config->getID()));
335 return $this->newDialog()
336 ->addHiddenInput('providerPHID', $selected_provider->getPHID())
337 ->addHiddenInput('mfa.start', 1)
338 ->addHiddenInput('mfa.enroll', 1)
339 ->setWidth(AphrontDialogView
::WIDTH_FORM
)
340 ->setTitle(pht('Add Authentication Factor'))
341 ->appendChild($form->buildLayoutView())
342 ->addSubmitButton(pht('Continue'))
343 ->addCancelButton($cancel_uri);
346 private function processEdit(AphrontRequest
$request) {
347 $viewer = $request->getUser();
348 $user = $this->getUser();
350 $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
351 'id = %d AND userPHID = %s',
352 $request->getInt('edit'),
355 return new Aphront404Response();
360 if ($request->isFormPost()) {
361 $name = $request->getStr('name');
362 if (!strlen($name)) {
363 $e_name = pht('Required');
365 'Authentication factors must have a name to identify them.');
369 $factor->setFactorName($name);
372 $user->updateMultiFactorEnrollment();
374 return id(new AphrontRedirectResponse())
375 ->setURI($this->getPanelURI('?id='.$factor->getID()));
378 $name = $factor->getFactorName();
381 $form = id(new AphrontFormView())
384 id(new AphrontFormTextControl())
386 ->setLabel(pht('Name'))
388 ->setError($e_name));
390 $dialog = id(new AphrontDialogView())
392 ->addHiddenInput('edit', $factor->getID())
393 ->setTitle(pht('Edit Authentication Factor'))
395 ->appendChild($form->buildLayoutView())
396 ->addSubmitButton(pht('Save'))
397 ->addCancelButton($this->getPanelURI());
399 return id(new AphrontDialogResponse())
400 ->setDialog($dialog);
403 private function processDelete(AphrontRequest
$request) {
404 $viewer = $request->getUser();
405 $user = $this->getUser();
407 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
410 $this->getPanelURI());
412 $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
413 'id = %d AND userPHID = %s',
414 $request->getInt('delete'),
417 return new Aphront404Response();
420 if ($request->isFormPost()) {
423 $log = PhabricatorUserLog
::initializeNewLog(
426 PhabricatorRemoveMultifactorUserLogType
::LOGTYPE
);
429 $user->updateMultiFactorEnrollment();
431 return id(new AphrontRedirectResponse())
432 ->setURI($this->getPanelURI());
435 $dialog = id(new AphrontDialogView())
437 ->addHiddenInput('delete', $factor->getID())
438 ->setTitle(pht('Delete Authentication Factor'))
441 'Really remove the authentication factor %s from your account?',
442 phutil_tag('strong', array(), $factor->getFactorName())))
443 ->addSubmitButton(pht('Remove Factor'))
444 ->addCancelButton($this->getPanelURI());
446 return id(new AphrontDialogResponse())
447 ->setDialog($dialog);
450 private function loadActiveMFAProviders() {
451 $viewer = $this->getViewer();
453 $providers = id(new PhabricatorAuthFactorProviderQuery())
457 PhabricatorAuthFactorProviderStatus
::STATUS_ACTIVE
,
461 $providers = mpull($providers, null, 'getPHID');
462 $providers = msortv($providers, 'newSortVector');