1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Make external link/refresh use provider IDs, switch external account MFA to one-shot

Summary:
Depends on D20113. Ref T6703. Continue moving toward a future where multiple copies of a given type of provider may exist.

Switch MFA from session-MFA at the start to one-shot MFA at the actual link action.

Add one-shot MFA to the unlink action. This theoretically prevents an attacker from unlinking an account while you're getting coffee, registering `alIce` which they control, adding a copy of your profile picture, and then trying to trick you into writing a private note with your personal secrets or something.

Test Plan: Linked and unlinked accounts. Refreshed account. Unlinked, then registered a new account. Unlinked, then relinked to my old account.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T6703

Differential Revision: https://secure.phabricator.com/D20117
This commit is contained in:
epriestley 2019-02-06 17:12:12 -08:00
parent e5ee656fff
commit d22495a820
8 changed files with 76 additions and 60 deletions

View file

@ -62,7 +62,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'validate/' => 'PhabricatorAuthValidateController', 'validate/' => 'PhabricatorAuthValidateController',
'finish/' => 'PhabricatorAuthFinishController', 'finish/' => 'PhabricatorAuthFinishController',
'unlink/(?P<id>\d+)/' => 'PhabricatorAuthUnlinkController', 'unlink/(?P<id>\d+)/' => 'PhabricatorAuthUnlinkController',
'(?P<action>link|refresh)/(?P<pkey>[^/]+)/' '(?P<action>link|refresh)/(?P<id>\d+)/'
=> 'PhabricatorAuthLinkController', => 'PhabricatorAuthLinkController',
'confirmlink/(?P<akey>[^/]+)/' 'confirmlink/(?P<akey>[^/]+)/'
=> 'PhabricatorAuthConfirmLinkController', => 'PhabricatorAuthConfirmLinkController',

View file

@ -20,7 +20,15 @@ final class PhabricatorAuthConfirmLinkController
$panel_uri = '/settings/panel/external/'; $panel_uri = '/settings/panel/external/';
if ($request->isFormPost()) { if ($request->isFormOrHisecPost()) {
$workflow_key = sprintf(
'account.link(%s)',
$account->getPHID());
$hisec_token = id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken($viewer, $request, $panel_uri);
$account->setUserPHID($viewer->getPHID()); $account->setUserPHID($viewer->getPHID());
$account->save(); $account->save();
@ -31,14 +39,7 @@ final class PhabricatorAuthConfirmLinkController
return id(new AphrontRedirectResponse())->setURI($panel_uri); return id(new AphrontRedirectResponse())->setURI($panel_uri);
} }
// TODO: Provide more information about the external account. Clicking $dialog = $this->newDialog()
// through this form blindly is dangerous.
// TODO: If the user has password authentication, require them to retype
// their password here.
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Confirm %s Account Link', $provider->getProviderName())) ->setTitle(pht('Confirm %s Account Link', $provider->getProviderName()))
->addCancelButton($panel_uri) ->addCancelButton($panel_uri)
->addSubmitButton(pht('Confirm Account Link')); ->addSubmitButton(pht('Confirm Account Link'));

View file

@ -213,19 +213,19 @@ abstract class PhabricatorAuthController extends PhabricatorController {
return array($account, $provider, $response); return array($account, $provider, $response);
} }
$provider = PhabricatorAuthProvider::getEnabledProviderByKey( $config = $account->getProviderConfig();
$account->getProviderKey()); if (!$config->getIsEnabled()) {
if (!$provider) {
$response = $this->renderError( $response = $this->renderError(
pht( pht(
'The account you are attempting to register with uses a nonexistent '. 'The account you are attempting to register with uses a disabled '.
'or disabled authentication provider (with key "%s"). An '. 'authentication provider ("%s"). An administrator may have '.
'administrator may have recently disabled this provider.', 'recently disabled this provider.',
$account->getProviderKey())); $config->getDisplayName()));
return array($account, $provider, $response); return array($account, $provider, $response);
} }
$provider = $config->getProvider();
return array($account, $provider, null); return array($account, $provider, null);
} }

View file

@ -6,14 +6,20 @@ final class PhabricatorAuthLinkController
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$action = $request->getURIData('action'); $action = $request->getURIData('action');
$provider_key = $request->getURIData('pkey');
$provider = PhabricatorAuthProvider::getEnabledProviderByKey( $id = $request->getURIData('id');
$provider_key);
if (!$provider) { $config = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->withIDs(array($id))
->withIsEnabled(true)
->executeOne();
if (!$config) {
return new Aphront404Response(); return new Aphront404Response();
} }
$provider = $config->getProvider();
switch ($action) { switch ($action) {
case 'link': case 'link':
if (!$provider->shouldAllowAccountLink()) { if (!$provider->shouldAllowAccountLink()) {
@ -37,15 +43,15 @@ final class PhabricatorAuthLinkController
return new Aphront400Response(); return new Aphront400Response();
} }
$account = id(new PhabricatorExternalAccount())->loadOneWhere( $accounts = id(new PhabricatorExternalAccountQuery())
'accountType = %s AND accountDomain = %s AND userPHID = %s', ->setViewer($viewer)
$provider->getProviderType(), ->withUserPHIDs(array($viewer->getPHID()))
$provider->getProviderDomain(), ->withProviderConfigPHIDs(array($config->getPHID()))
$viewer->getPHID()); ->execute();
switch ($action) { switch ($action) {
case 'link': case 'link':
if ($account) { if ($accounts) {
return $this->renderErrorPage( return $this->renderErrorPage(
pht('Account Already Linked'), pht('Account Already Linked'),
array( array(
@ -56,7 +62,7 @@ final class PhabricatorAuthLinkController
} }
break; break;
case 'refresh': case 'refresh':
if (!$account) { if (!$accounts) {
return $this->renderErrorPage( return $this->renderErrorPage(
pht('No Account Linked'), pht('No Account Linked'),
array( array(
@ -76,11 +82,6 @@ final class PhabricatorAuthLinkController
switch ($action) { switch ($action) {
case 'link': case 'link':
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$panel_uri);
$form = $provider->buildLinkForm($this); $form = $provider->buildLinkForm($this);
break; break;
case 'refresh': case 'refresh':

View file

@ -31,7 +31,7 @@ final class PhabricatorAuthUnlinkController
$confirmations = $request->getStrList('confirmations'); $confirmations = $request->getStrList('confirmations');
$confirmations = array_fuse($confirmations); $confirmations = array_fuse($confirmations);
if (!$request->isFormPost() || !isset($confirmations['unlink'])) { if (!$request->isFormOrHisecPost() || !isset($confirmations['unlink'])) {
return $this->renderConfirmDialog($confirmations, $config, $done_uri); return $this->renderConfirmDialog($confirmations, $config, $done_uri);
} }
@ -59,6 +59,14 @@ final class PhabricatorAuthUnlinkController
} }
} }
$workflow_key = sprintf(
'account.unlink(%s)',
$account->getPHID());
$hisec_token = id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken($viewer, $request, $done_uri);
$account->delete(); $account->delete();
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(

View file

@ -21,6 +21,7 @@ final class PhabricatorExternalAccountQuery
private $userPHIDs; private $userPHIDs;
private $needImages; private $needImages;
private $accountSecrets; private $accountSecrets;
private $providerConfigPHIDs;
public function withUserPHIDs(array $user_phids) { public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids; $this->userPHIDs = $user_phids;
@ -62,6 +63,11 @@ final class PhabricatorExternalAccountQuery
return $this; return $this;
} }
public function withProviderConfigPHIDs(array $phids) {
$this->providerConfigPHIDs = $phids;
return $this;
}
public function newResultObject() { public function newResultObject() {
return new PhabricatorExternalAccount(); return new PhabricatorExternalAccount();
} }
@ -181,6 +187,13 @@ final class PhabricatorExternalAccountQuery
$this->accountSecrets); $this->accountSecrets);
} }
if ($this->providerConfigPHIDs !== null) {
$where[] = qsprintf(
$conn,
'providerConfigPHID IN (%Ls)',
$this->providerConfigPHIDs);
}
return $where; return $where;
} }

View file

@ -157,13 +157,10 @@ final class PhabricatorPeopleProfilePictureController
continue; continue;
} }
$provider = PhabricatorAuthProvider::getEnabledProviderByKey( $config = $account->getProviderConfig();
$account->getProviderKey()); $provider = $config->getProvider();
if ($provider) {
$tip = pht('Picture From %s', $provider->getProviderName()); $tip = pht('Picture From %s', $provider->getProviderName());
} else {
$tip = pht('Picture From External Account');
}
if ($file->isTransformableImage()) { if ($file->isTransformableImage()) {
$images[$file->getPHID()] = array( $images[$file->getPHID()] = array(

View file

@ -44,18 +44,14 @@ final class PhabricatorExternalAccountsSettingsPanel
foreach ($accounts as $account) { foreach ($accounts as $account) {
$item = new PHUIObjectItemView(); $item = new PHUIObjectItemView();
$provider = idx($providers, $account->getProviderKey()); $config = $account->getProviderConfig();
if ($provider) { $provider = $config->getProvider();
$item->setHeader($provider->getProviderName()); $item->setHeader($provider->getProviderName());
$can_unlink = $provider->shouldAllowAccountUnlink(); $can_unlink = $provider->shouldAllowAccountUnlink();
if (!$can_unlink) { if (!$can_unlink) {
$item->addAttribute(pht('Permanently Linked')); $item->addAttribute(pht('Permanently Linked'));
} }
} else {
$item->setHeader(
pht('Unknown Account ("%s")', $account->getProviderKey()));
$can_unlink = true;
}
$can_login = $account->isUsableForLogin(); $can_login = $account->isUsableForLogin();
if (!$can_login) { if (!$can_login) {
@ -65,12 +61,12 @@ final class PhabricatorExternalAccountsSettingsPanel
'account provider).')); 'account provider).'));
} }
$can_refresh = $provider && $provider->shouldAllowAccountRefresh(); $can_refresh = $provider->shouldAllowAccountRefresh();
if ($can_refresh) { if ($can_refresh) {
$item->addAction( $item->addAction(
id(new PHUIListItemView()) id(new PHUIListItemView())
->setIcon('fa-refresh') ->setIcon('fa-refresh')
->setHref('/auth/refresh/'.$account->getProviderKey().'/')); ->setHref('/auth/refresh/'.$config->getID().'/'));
} }
$item->addAction( $item->addAction(
@ -94,14 +90,15 @@ final class PhabricatorExternalAccountsSettingsPanel
->setNoDataString( ->setNoDataString(
pht('Your account is linked with all available providers.')); pht('Your account is linked with all available providers.'));
$accounts = mpull($accounts, null, 'getProviderKey');
$configs = id(new PhabricatorAuthProviderConfigQuery()) $configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer) ->setViewer($viewer)
->withIsEnabled(true) ->withIsEnabled(true)
->execute(); ->execute();
$configs = msort($configs, 'getSortVector'); $configs = msort($configs, 'getSortVector');
$account_map = mgroup($accounts, 'getProviderConfigPHID');
foreach ($configs as $config) { foreach ($configs as $config) {
$provider = $config->getProvider(); $provider = $config->getProvider();
@ -110,12 +107,11 @@ final class PhabricatorExternalAccountsSettingsPanel
} }
// Don't show the user providers they already have linked. // Don't show the user providers they already have linked.
$provider_key = $config->getProvider()->getProviderKey(); if (isset($account_map[$config->getPHID()])) {
if (isset($accounts[$provider_key])) {
continue; continue;
} }
$link_uri = '/auth/link/'.$provider->getProviderKey().'/'; $link_uri = '/auth/link/'.$config->getID().'/';
$link_button = id(new PHUIButtonView()) $link_button = id(new PHUIButtonView())
->setTag('a') ->setTag('a')