From 30f6405a865441162c8e2dfd8fe2cf34ac314706 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Aug 2014 12:04:13 -0700 Subject: [PATCH] Add an explicit temporary token management page to Settings Summary: Ref T5506. This makes it easier to understand and manage temporary tokens. Eventually this could be more user-friendly, since it's relatively difficult to understand what this screen means. My short-term goal is just to make the next change easier to implement and test. The next diff will close a small security weakness: if you change your email address, password reset links which were sent to the old address are still valid. Although an attacker would need substantial access to exploit this (essentially, it would just make it easier for them to re-compromise an already compromised account), it's a bit surprising. In the next diff, email address changes will invalidate outstanding password reset links. Test Plan: - Viewed outstanding tokens. - Added tokens to the list by making "Forgot your password?" requests. - Revoked tokens individually. - Revoked all tokens. - Tried to use a revoked token. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T5506 Differential Revision: https://secure.phabricator.com/D10133 --- src/__phutil_library_map__.php | 4 + .../PhabricatorAuthApplication.php | 2 + .../PhabricatorAuthRevokeTokenController.php | 78 ++++++++++++++ .../storage/PhabricatorAuthTemporaryToken.php | 28 ++++- .../panel/PhabricatorSettingsPanelTokens.php | 100 ++++++++++++++++++ 5 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php create mode 100644 src/applications/settings/panel/PhabricatorSettingsPanelTokens.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0ff38ba911..8fc7cdeb50 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1192,6 +1192,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php', 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', + 'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', @@ -2146,6 +2147,7 @@ phutil_register_library_map(array( 'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php', 'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php', 'PhabricatorSettingsPanelSessions' => 'applications/settings/panel/PhabricatorSettingsPanelSessions.php', + 'PhabricatorSettingsPanelTokens' => 'applications/settings/panel/PhabricatorSettingsPanelTokens.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckAPC' => 'applications/config/check/PhabricatorSetupCheckAPC.php', 'PhabricatorSetupCheckAphlict' => 'applications/notification/setup/PhabricatorSetupCheckAphlict.php', @@ -3986,6 +3988,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', + 'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController', 'PhabricatorAuthSession' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', @@ -4999,6 +5002,7 @@ phutil_register_library_map(array( 'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSessions' => 'PhabricatorSettingsPanel', + 'PhabricatorSettingsPanelTokens' => 'PhabricatorSettingsPanel', 'PhabricatorSetupCheckAPC' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckAphlict' => 'PhabricatorSetupCheck', 'PhabricatorSetupCheckAuth' => 'PhabricatorSetupCheck', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 132c63b0a5..4a9760feeb 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -103,6 +103,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { => 'PhabricatorAuthConfirmLinkController', 'session/terminate/(?P[^/]+)/' => 'PhabricatorAuthTerminateSessionController', + 'token/revoke/(?P[^/]+)/' + => 'PhabricatorAuthRevokeTokenController', 'session/downgrade/' => 'PhabricatorAuthDowngradeSessionController', 'multifactor/' diff --git a/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php new file mode 100644 index 0000000000..1b78a9c829 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php @@ -0,0 +1,78 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $is_all = ($this->id === 'all'); + + $query = id(new PhabricatorAuthTemporaryTokenQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($viewer->getPHID())); + if (!$is_all) { + $query->withIDs(array($this->id)); + } + + $tokens = $query->execute(); + foreach ($tokens as $key => $token) { + if (!$token->isRevocable()) { + // Don't revoke unrevocable tokens. + unset($tokens[$key]); + } + } + + $panel_uri = '/settings/panel/tokens/'; + + if (!$tokens) { + return $this->newDialog() + ->setTitle(pht('No Matching Tokens')) + ->appendParagraph( + pht('There are no matching tokens to revoke.')) + ->appendParagraph( + pht( + '(Some types of token can not be revoked, and you can not revoke '. + 'tokens which have already expired.)')) + ->addCancelButton($panel_uri); + } + + if ($request->isDialogFormPost()) { + foreach ($tokens as $token) { + $token->setTokenExpires(PhabricatorTime::getNow() - 1)->save(); + } + return id(new AphrontRedirectResponse())->setURI($panel_uri); + } + + if ($is_all) { + $title = pht('Revoke Tokens?'); + $short = pht('Revoke Tokens'); + $body = pht( + 'Really revoke all tokens? Among other temporary authorizations, '. + 'this will disable any outstanding password reset or account '. + 'recovery links.'); + } else { + $title = pht('Revoke Token?'); + $short = pht('Revoke Token'); + $body = pht( + 'Really revoke this token? Any temporary authorization it enables '. + 'will be disabled.'); + } + + return $this->newDialog() + ->setTitle($title) + ->setShortTitle($short) + ->appendParagraph($body) + ->addSubmitButton(pht('Revoke')) + ->addCancelButton($panel_uri); + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php index 6956afda60..970ea7ef2b 100644 --- a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php +++ b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php @@ -17,6 +17,33 @@ final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO ) + parent::getConfiguration(); } + public function getTokenReadableTypeName() { + // Eventually, it would be nice to let applications implement token types + // so we can put this in modular subclasses. + switch ($this->tokenType) { + case PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE: + return pht('One-Time Login Token'); + case PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE: + return pht('Password Reset Token'); + } + + return $this->tokenType; + } + + public function isRevocable() { + if ($this->tokenExpires < time()) { + return false; + } + + switch ($this->tokenType) { + case PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE: + case PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE: + return true; + } + + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -40,5 +67,4 @@ final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO return null; } - } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelTokens.php b/src/applications/settings/panel/PhabricatorSettingsPanelTokens.php new file mode 100644 index 0000000000..7aa7aaaed4 --- /dev/null +++ b/src/applications/settings/panel/PhabricatorSettingsPanelTokens.php @@ -0,0 +1,100 @@ +getUser(); + + $tokens = id(new PhabricatorAuthTemporaryTokenQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($viewer->getPHID())) + ->execute(); + + $rows = array(); + foreach ($tokens as $token) { + + if ($token->isRevocable()) { + $button = javelin_tag( + 'a', + array( + 'href' => '/auth/token/revoke/'.$token->getID().'/', + 'class' => 'small grey button', + 'sigil' => 'workflow', + ), + pht('Revoke')); + } else { + $button = javelin_tag( + 'a', + array( + 'class' => 'small grey button disabled', + ), + pht('Revoke')); + } + + if ($token->getTokenExpires() >= time()) { + $expiry = phabricator_datetime($token->getTokenExpires(), $viewer); + } else { + $expiry = pht('Expired'); + } + + $rows[] = array( + $token->getTokenReadableTypeName(), + $expiry, + $button, + ); + } + + $table = new AphrontTableView($rows); + $table->setNoDataString(pht("You don't have any active tokens.")); + $table->setHeaders( + array( + pht('Type'), + pht('Expires'), + pht(''), + )); + $table->setColumnClasses( + array( + 'wide', + 'right', + 'action', + )); + + + $terminate_icon = id(new PHUIIconView()) + ->setIconFont('fa-exclamation-triangle'); + $terminate_button = id(new PHUIButtonView()) + ->setText(pht('Revoke All')) + ->setHref('/auth/token/revoke/all/') + ->setTag('a') + ->setWorkflow(true) + ->setIcon($terminate_icon); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Temporary Tokens')) + ->addActionLink($terminate_button); + + $panel = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($table); + + return $panel; + } + +}