diff --git a/lang/en.yml b/lang/en.yml index ce663fad6..a1f0dfa3e 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -259,6 +259,7 @@ en: ADDGROUP: 'Add group' BUTTONCHANGEPASSWORD: 'Change Password' BUTTONLOGIN: 'Log in' + BUTTONLOGOUT: 'Log out' BUTTONLOGINOTHER: 'Log in as someone else' BUTTONLOSTPASSWORD: 'I''ve lost my password' CONFIRMNEWPASSWORD: 'Confirm New Password' @@ -354,9 +355,11 @@ en: BUTTONSEND: 'Send me the password reset link' CHANGEPASSWORDBELOW: 'You can change your password below.' CHANGEPASSWORDHEADER: 'Change your password' + CONFIRMLOGOUT: 'Please click the button below to confirm that you wish to log out.' ENTERNEWPASSWORD: 'Please enter a new password.' ERRORPASSWORDPERMISSION: 'You must be logged in in order to change your password!' LOGIN: 'Log in' + LOGOUT: 'Log out' LOSTPASSWORDHEADER: 'Lost Password' NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.' NOTERESETLINKINVALID: '
The password reset link is invalid or expired.
You can request a new one here or change your password after you logged in.
' diff --git a/src/Security/CMSSecurity.php b/src/Security/CMSSecurity.php index 16cdcb6cf..9ed9c0b95 100644 --- a/src/Security/CMSSecurity.php +++ b/src/Security/CMSSecurity.php @@ -89,9 +89,9 @@ class CMSSecurity extends Security return $this; } - protected function getLoginMessage(&$messageType = null) + protected function getSessionMessage(&$messageType = null) { - $message = parent::getLoginMessage($messageType); + $message = parent::getSessionMessage($messageType); if ($message) { return $message; } diff --git a/src/Security/LogoutForm.php b/src/Security/LogoutForm.php new file mode 100644 index 000000000..e8851c51e --- /dev/null +++ b/src/Security/LogoutForm.php @@ -0,0 +1,81 @@ +setController($controller); + + if (!$fields) { + $fields = $this->getFormFields(); + } + if (!$actions) { + $actions = $this->getFormActions(); + } + + parent::__construct($controller, $name, $fields, $actions); + + $this->setFormAction(Security::logout_url()); + } + + /** + * Build the FieldList for the logout form + * + * @return FieldList + */ + protected function getFormFields() + { + $fields = FieldList::create(); + + $controller = $this->getController(); + $backURL = $controller->getBackURL() + ?: $controller->getReturnReferer(); + + // Protect against infinite redirection back to the logout URL after logging out + if (!$backURL || Director::makeRelative($backURL) === $controller->getRequest()->getURL()) { + $backURL = Director::baseURL(); + } + + $fields->push(HiddenField::create('BackURL', 'BackURL', $backURL)); + + return $fields; + } + + /** + * Build default logout form action FieldList + * + * @return FieldList + */ + protected function getFormActions() + { + $actions = FieldList::create( + FormAction::create('doLogout', _t('SilverStripe\\Security\\Member.BUTTONLOGOUT', "Log out")) + ); + + return $actions; + } +} diff --git a/src/Security/MemberAuthenticator/LoginHandler.php b/src/Security/MemberAuthenticator/LoginHandler.php index 31a2b00c4..8bc108cfb 100644 --- a/src/Security/MemberAuthenticator/LoginHandler.php +++ b/src/Security/MemberAuthenticator/LoginHandler.php @@ -195,7 +195,7 @@ class LoginHandler extends RequestHandler 'Welcome Back, {firstname}', ['firstname' => $member->FirstName] ); - Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_GOOD); + Security::singleton()->setSessionMessage($message, ValidationResult::TYPE_GOOD); } // Redirect back diff --git a/src/Security/MemberAuthenticator/LogoutHandler.php b/src/Security/MemberAuthenticator/LogoutHandler.php index 21b2a4381..22909aeaf 100644 --- a/src/Security/MemberAuthenticator/LogoutHandler.php +++ b/src/Security/MemberAuthenticator/LogoutHandler.php @@ -2,11 +2,15 @@ namespace SilverStripe\Security\MemberAuthenticator; +use SilverStripe\Control\Director; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\IdentityStore; +use SilverStripe\Security\LogoutForm; use SilverStripe\Security\Member; use SilverStripe\Security\Security; +use SilverStripe\Security\SecurityToken; /** * Class LogoutHandler handles logging out Members from their session and/or cookie. @@ -27,7 +31,8 @@ class LogoutHandler extends RequestHandler * @var array */ private static $allowed_actions = [ - 'logout' + 'logout', + 'LogoutForm' ]; @@ -45,20 +50,64 @@ class LogoutHandler extends RequestHandler { $member = Security::getCurrentUser(); + // If the user doesn't have a security token, show them a form where they can get one. + // This protects against nuisance CSRF attacks to log out users. + if ($member && !SecurityToken::inst()->checkRequest($this->getRequest())) { + Security::singleton()->setSessionMessage( + _t( + 'SilverStripe\\Security\\Security.CONFIRMLOGOUT', + "Please click the button below to confirm that you wish to log out." + ), + ValidationResult::TYPE_WARNING + ); + + return [ + 'Form' => $this->logoutForm() + ]; + } + return $this->doLogOut($member); } /** - * + * @return LogoutForm + */ + public function logoutForm() + { + return LogoutForm::create($this); + } + + /** * @param Member $member - * @return bool|Member Return a member if something goes wrong + * @return HTTPResponse */ public function doLogOut($member) { + $this->extend('beforeLogout'); + if ($member instanceof Member) { Injector::inst()->get(IdentityStore::class)->logOut($this->getRequest()); } - return true; + if (Security::getCurrentUser()) { + $this->extend('failedLogout'); + } else { + $this->extend('afterLogout'); + } + + return $this->redirectAfterLogout(); + } + + /** + * @return HTTPResponse + */ + protected function redirectAfterLogout() + { + $backURL = $this->getBackURL(); + if ($backURL) { + return $this->redirect($backURL); + } + + return $this->redirect(Director::absoluteBaseURL()); } } diff --git a/src/Security/Security.php b/src/Security/Security.php index 3d1bff936..ff008dd30 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -4,6 +4,7 @@ namespace SilverStripe\Security; use LogicException; use Page; +use ReflectionClass; use SilverStripe\CMS\Controllers\ModelAsController; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; @@ -391,7 +392,7 @@ class Security extends Controller implements TemplateGlobalProvider $message = $messageSet['default']; } - static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING); + static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING); $loginResponse = static::singleton()->login(); if ($loginResponse instanceof HTTPResponse) { return $loginResponse; @@ -406,7 +407,7 @@ class Security extends Controller implements TemplateGlobalProvider $message = $messageSet['default']; } - static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING); + static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING); Session::set("BackURL", $_SERVER['REQUEST_URI']); @@ -481,54 +482,6 @@ class Security extends Controller implements TemplateGlobalProvider return 1; } - /** - * Log the currently logged in user out - * - * Logging out without ID-parameter in the URL, will log the user out of all applicable Authenticators. - * - * Adding an ID will only log the user out of that Authentication method. - * - * Logging out of Default will always completely log out the user. - * - * @param bool $redirect Redirect the user back to where they came. - * - If it's false, the code calling logout() is - * responsible for sending the user where-ever - * they should go. - * @return HTTPResponse|null - */ - public function logout($redirect = true) - { - $this->extend('beforeMemberLoggedOut'); - $member = static::getCurrentUser(); - - if ($member) { // If we don't have a member, there's not much to log out. - /** @var array|Authenticator[] $authenticators */ - $authenticators = $this->getApplicableAuthenticators(Authenticator::LOGOUT); - - /** @var Authenticator[] $authenticator */ - foreach ($authenticators as $name => $authenticator) { - $handler = $authenticator->getLogOutHandler(Controller::join_links($this->Link(), 'logout')); - $this->delegateToHandler($handler, $name); - } - // In the rare case, but plausible with e.g. an external IdentityStore, the user is not logged out. - if (static::getCurrentUser() !== null) { - $this->extend('failureMemberLoggedOut', $authenticator); - - return $this->redirectBack(); - } - $this->extend('successMemberLoggedOut', $authenticator); - // Member is successfully logged out. Write possible changes to the database. - $member->write(); - } - $this->extend('afterMemberLoggedOut'); - - if ($redirect && (!$this->getResponse()->isFinished())) { - return $this->redirectBack(); - } - - return null; - } - /** * Perform pre-login checking and prepare a response if available prior to login * @@ -558,7 +511,7 @@ class Security extends Controller implements TemplateGlobalProvider // This step is necessary in cases such as automatic redirection where a user is authenticated // upon landing on an SSL secured site and is automatically logged in, or some other case // where the user has permissions to continue but is not given the option. - if (!$this->getLoginMessage() + if (!$this->getSessionMessage() && ($member = static::getCurrentUser()) && $member->exists() && $this->getRequest()->requestVar('BackURL') @@ -605,18 +558,18 @@ class Security extends Controller implements TemplateGlobalProvider * @param array|Form[] $forms * @return string */ - protected function generateLoginFormSet($forms) + protected function generateTabbedFormSet($forms) { if (count($forms) === 1) { return $forms; } - $viewData = new ArrayData(array( + $viewData = new ArrayData([ 'Forms' => new ArrayList($forms), - )); + ]); return $viewData->renderWith( - $this->getTemplatesFor('MultiAuthenticatorLogin') + $this->getTemplatesFor('MultiAuthenticatorTabbedForms') ); } @@ -626,7 +579,7 @@ class Security extends Controller implements TemplateGlobalProvider * @param string &$messageType Type of message, if available, passed back to caller * @return string Message in HTML format */ - protected function getLoginMessage(&$messageType = null) + protected function getSessionMessage(&$messageType = null) { $message = Session::get('Security.Message.message'); $messageType = null; @@ -650,7 +603,7 @@ class Security extends Controller implements TemplateGlobalProvider * @param string $messageType Message type. One of ValidationResult::TYPE_* * @param string $messageCast Message cast. One of ValidationResult::CAST_* */ - public function setLoginMessage( + public function setSessionMessage( $message, $messageType = ValidationResult::TYPE_WARNING, $messageCast = ValidationResult::CAST_TEXT @@ -663,7 +616,7 @@ class Security extends Controller implements TemplateGlobalProvider /** * Clear login message */ - public static function clearLoginMessage() + public static function clearSessionMessage() { Session::clear('Security.Message'); } @@ -678,7 +631,6 @@ class Security extends Controller implements TemplateGlobalProvider * @param null|HTTPRequest $request * @param int $service * @return HTTPResponse|string Returns the "login" page as HTML code. - * @throws HTTPResponse_Exception */ public function login($request = null, $service = Authenticator::LOGIN) { @@ -692,30 +644,9 @@ class Security extends Controller implements TemplateGlobalProvider $request = $this->getRequest(); } - if ($request && $request->param('ID')) { - $authName = $request->param('ID'); - } + $handlers = $this->getServiceAuthenticatorsFromRequest($service, $request); $link = $this->Link('login'); - - // Delegate to a single handler - Security/login/