mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW: Add CSRF token to logout action
This commit is contained in:
parent
4d73982263
commit
5d27dccd60
@ -259,6 +259,7 @@ en:
|
|||||||
ADDGROUP: 'Add group'
|
ADDGROUP: 'Add group'
|
||||||
BUTTONCHANGEPASSWORD: 'Change Password'
|
BUTTONCHANGEPASSWORD: 'Change Password'
|
||||||
BUTTONLOGIN: 'Log in'
|
BUTTONLOGIN: 'Log in'
|
||||||
|
BUTTONLOGOUT: 'Log out'
|
||||||
BUTTONLOGINOTHER: 'Log in as someone else'
|
BUTTONLOGINOTHER: 'Log in as someone else'
|
||||||
BUTTONLOSTPASSWORD: 'I''ve lost my password'
|
BUTTONLOSTPASSWORD: 'I''ve lost my password'
|
||||||
CONFIRMNEWPASSWORD: 'Confirm New Password'
|
CONFIRMNEWPASSWORD: 'Confirm New Password'
|
||||||
@ -354,9 +355,11 @@ en:
|
|||||||
BUTTONSEND: 'Send me the password reset link'
|
BUTTONSEND: 'Send me the password reset link'
|
||||||
CHANGEPASSWORDBELOW: 'You can change your password below.'
|
CHANGEPASSWORDBELOW: 'You can change your password below.'
|
||||||
CHANGEPASSWORDHEADER: 'Change your password'
|
CHANGEPASSWORDHEADER: 'Change your password'
|
||||||
|
CONFIRMLOGOUT: 'Please click the button below to confirm that you wish to log out.'
|
||||||
ENTERNEWPASSWORD: 'Please enter a new password.'
|
ENTERNEWPASSWORD: 'Please enter a new password.'
|
||||||
ERRORPASSWORDPERMISSION: 'You must be logged in in order to change your password!'
|
ERRORPASSWORDPERMISSION: 'You must be logged in in order to change your password!'
|
||||||
LOGIN: 'Log in'
|
LOGIN: 'Log in'
|
||||||
|
LOGOUT: 'Log out'
|
||||||
LOSTPASSWORDHEADER: 'Lost Password'
|
LOSTPASSWORDHEADER: 'Lost Password'
|
||||||
NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.'
|
NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.'
|
||||||
NOTERESETLINKINVALID: '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="{link1}">here</a> or change your password after you <a href="{link2}">logged in</a>.</p>'
|
NOTERESETLINKINVALID: '<p>The password reset link is invalid or expired.</p><p>You can request a new one <a href="{link1}">here</a> or change your password after you <a href="{link2}">logged in</a>.</p>'
|
||||||
|
@ -89,9 +89,9 @@ class CMSSecurity extends Security
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getLoginMessage(&$messageType = null)
|
protected function getSessionMessage(&$messageType = null)
|
||||||
{
|
{
|
||||||
$message = parent::getLoginMessage($messageType);
|
$message = parent::getSessionMessage($messageType);
|
||||||
if ($message) {
|
if ($message) {
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
81
src/Security/LogoutForm.php
Normal file
81
src/Security/LogoutForm.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Control\RequestHandler;
|
||||||
|
use SilverStripe\Control\Session;
|
||||||
|
use SilverStripe\Forms\FieldList;
|
||||||
|
use SilverStripe\Forms\Form;
|
||||||
|
use SilverStripe\Forms\FormAction;
|
||||||
|
use SilverStripe\Forms\HiddenField;
|
||||||
|
use SilverStripe\Forms\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out form to display to users who arrive at 'Security/logout' without a
|
||||||
|
* CSRF token. It's preferable to link to {@link Security::logout_url()}
|
||||||
|
* directly - we only use a form so that we can preserve the "BackURL" if set
|
||||||
|
*/
|
||||||
|
class LogoutForm extends Form
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
RequestHandler $controller = null,
|
||||||
|
$name = self::DEFAULT_NAME,
|
||||||
|
FieldList $fields = null,
|
||||||
|
FieldList $actions = null,
|
||||||
|
Validator $validator = null
|
||||||
|
) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -195,7 +195,7 @@ class LoginHandler extends RequestHandler
|
|||||||
'Welcome Back, {firstname}',
|
'Welcome Back, {firstname}',
|
||||||
['firstname' => $member->FirstName]
|
['firstname' => $member->FirstName]
|
||||||
);
|
);
|
||||||
Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_GOOD);
|
Security::singleton()->setSessionMessage($message, ValidationResult::TYPE_GOOD);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back
|
// Redirect back
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Security\MemberAuthenticator;
|
namespace SilverStripe\Security\MemberAuthenticator;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
use SilverStripe\Control\RequestHandler;
|
use SilverStripe\Control\RequestHandler;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
|
use SilverStripe\ORM\ValidationResult;
|
||||||
use SilverStripe\Security\IdentityStore;
|
use SilverStripe\Security\IdentityStore;
|
||||||
|
use SilverStripe\Security\LogoutForm;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\Security\SecurityToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LogoutHandler handles logging out Members from their session and/or cookie.
|
* Class LogoutHandler handles logging out Members from their session and/or cookie.
|
||||||
@ -27,7 +31,8 @@ class LogoutHandler extends RequestHandler
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private static $allowed_actions = [
|
private static $allowed_actions = [
|
||||||
'logout'
|
'logout',
|
||||||
|
'LogoutForm'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@ -45,20 +50,64 @@ class LogoutHandler extends RequestHandler
|
|||||||
{
|
{
|
||||||
$member = Security::getCurrentUser();
|
$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 $this->doLogOut($member);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @return LogoutForm
|
||||||
|
*/
|
||||||
|
public function logoutForm()
|
||||||
|
{
|
||||||
|
return LogoutForm::create($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* @param Member $member
|
* @param Member $member
|
||||||
* @return bool|Member Return a member if something goes wrong
|
* @return HTTPResponse
|
||||||
*/
|
*/
|
||||||
public function doLogOut($member)
|
public function doLogOut($member)
|
||||||
{
|
{
|
||||||
|
$this->extend('beforeLogout');
|
||||||
|
|
||||||
if ($member instanceof Member) {
|
if ($member instanceof Member) {
|
||||||
Injector::inst()->get(IdentityStore::class)->logOut($this->getRequest());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ namespace SilverStripe\Security;
|
|||||||
|
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use Page;
|
use Page;
|
||||||
|
use ReflectionClass;
|
||||||
use SilverStripe\CMS\Controllers\ModelAsController;
|
use SilverStripe\CMS\Controllers\ModelAsController;
|
||||||
use SilverStripe\Control\Controller;
|
use SilverStripe\Control\Controller;
|
||||||
use SilverStripe\Control\Director;
|
use SilverStripe\Control\Director;
|
||||||
@ -391,7 +392,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
$message = $messageSet['default'];
|
$message = $messageSet['default'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING);
|
||||||
$loginResponse = static::singleton()->login();
|
$loginResponse = static::singleton()->login();
|
||||||
if ($loginResponse instanceof HTTPResponse) {
|
if ($loginResponse instanceof HTTPResponse) {
|
||||||
return $loginResponse;
|
return $loginResponse;
|
||||||
@ -406,7 +407,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
$message = $messageSet['default'];
|
$message = $messageSet['default'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING);
|
||||||
|
|
||||||
Session::set("BackURL", $_SERVER['REQUEST_URI']);
|
Session::set("BackURL", $_SERVER['REQUEST_URI']);
|
||||||
|
|
||||||
@ -481,54 +482,6 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
return 1;
|
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 <i>always</i> 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
|
* 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
|
// 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
|
// 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.
|
// where the user has permissions to continue but is not given the option.
|
||||||
if (!$this->getLoginMessage()
|
if (!$this->getSessionMessage()
|
||||||
&& ($member = static::getCurrentUser())
|
&& ($member = static::getCurrentUser())
|
||||||
&& $member->exists()
|
&& $member->exists()
|
||||||
&& $this->getRequest()->requestVar('BackURL')
|
&& $this->getRequest()->requestVar('BackURL')
|
||||||
@ -605,18 +558,18 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
* @param array|Form[] $forms
|
* @param array|Form[] $forms
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function generateLoginFormSet($forms)
|
protected function generateTabbedFormSet($forms)
|
||||||
{
|
{
|
||||||
if (count($forms) === 1) {
|
if (count($forms) === 1) {
|
||||||
return $forms;
|
return $forms;
|
||||||
}
|
}
|
||||||
|
|
||||||
$viewData = new ArrayData(array(
|
$viewData = new ArrayData([
|
||||||
'Forms' => new ArrayList($forms),
|
'Forms' => new ArrayList($forms),
|
||||||
));
|
]);
|
||||||
|
|
||||||
return $viewData->renderWith(
|
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
|
* @param string &$messageType Type of message, if available, passed back to caller
|
||||||
* @return string Message in HTML format
|
* @return string Message in HTML format
|
||||||
*/
|
*/
|
||||||
protected function getLoginMessage(&$messageType = null)
|
protected function getSessionMessage(&$messageType = null)
|
||||||
{
|
{
|
||||||
$message = Session::get('Security.Message.message');
|
$message = Session::get('Security.Message.message');
|
||||||
$messageType = null;
|
$messageType = null;
|
||||||
@ -650,7 +603,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
* @param string $messageType Message type. One of ValidationResult::TYPE_*
|
* @param string $messageType Message type. One of ValidationResult::TYPE_*
|
||||||
* @param string $messageCast Message cast. One of ValidationResult::CAST_*
|
* @param string $messageCast Message cast. One of ValidationResult::CAST_*
|
||||||
*/
|
*/
|
||||||
public function setLoginMessage(
|
public function setSessionMessage(
|
||||||
$message,
|
$message,
|
||||||
$messageType = ValidationResult::TYPE_WARNING,
|
$messageType = ValidationResult::TYPE_WARNING,
|
||||||
$messageCast = ValidationResult::CAST_TEXT
|
$messageCast = ValidationResult::CAST_TEXT
|
||||||
@ -663,7 +616,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
/**
|
/**
|
||||||
* Clear login message
|
* Clear login message
|
||||||
*/
|
*/
|
||||||
public static function clearLoginMessage()
|
public static function clearSessionMessage()
|
||||||
{
|
{
|
||||||
Session::clear('Security.Message');
|
Session::clear('Security.Message');
|
||||||
}
|
}
|
||||||
@ -678,7 +631,6 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
* @param null|HTTPRequest $request
|
* @param null|HTTPRequest $request
|
||||||
* @param int $service
|
* @param int $service
|
||||||
* @return HTTPResponse|string Returns the "login" page as HTML code.
|
* @return HTTPResponse|string Returns the "login" page as HTML code.
|
||||||
* @throws HTTPResponse_Exception
|
|
||||||
*/
|
*/
|
||||||
public function login($request = null, $service = Authenticator::LOGIN)
|
public function login($request = null, $service = Authenticator::LOGIN)
|
||||||
{
|
{
|
||||||
@ -692,30 +644,9 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request && $request->param('ID')) {
|
$handlers = $this->getServiceAuthenticatorsFromRequest($service, $request);
|
||||||
$authName = $request->param('ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
$link = $this->Link('login');
|
$link = $this->Link('login');
|
||||||
|
|
||||||
// Delegate to a single handler - Security/login/<authname>/...
|
|
||||||
if ($authName && $this->hasAuthenticator($authName)) {
|
|
||||||
if ($request) {
|
|
||||||
$request->shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
$authenticator = $this->getAuthenticator($authName);
|
|
||||||
|
|
||||||
if (!$authenticator->supportedServices() & $service) {
|
|
||||||
throw new HTTPResponse_Exception('Invalid Authenticator "' . $authName . '" for login action', 418);
|
|
||||||
}
|
|
||||||
|
|
||||||
$handlers = [$authName => $authenticator];
|
|
||||||
} else {
|
|
||||||
// Delegate to all of them, building a tabbed view - Security/login
|
|
||||||
$handlers = $this->getApplicableAuthenticators($service);
|
|
||||||
}
|
|
||||||
|
|
||||||
array_walk(
|
array_walk(
|
||||||
$handlers,
|
$handlers,
|
||||||
function (Authenticator &$auth, $name) use ($link) {
|
function (Authenticator &$auth, $name) use ($link) {
|
||||||
@ -726,22 +657,137 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
return $this->delegateToMultipleHandlers(
|
return $this->delegateToMultipleHandlers(
|
||||||
$handlers,
|
$handlers,
|
||||||
_t('Security.LOGIN', 'Log in'),
|
_t('Security.LOGIN', 'Log in'),
|
||||||
$this->getTemplatesFor('login')
|
$this->getTemplatesFor('login'),
|
||||||
|
[$this, 'aggregateTabbedForms']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
|
* Log the currently logged in user out
|
||||||
* This is used to built the log-in page where there are multiple authenticators active.
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param null|HTTPRequest $request
|
||||||
|
* @param int $service
|
||||||
|
* @return HTTPResponse|string
|
||||||
|
*/
|
||||||
|
public function logout($request = null, $service = Authenticator::LOGOUT)
|
||||||
|
{
|
||||||
|
$authName = null;
|
||||||
|
|
||||||
|
if (!$request) {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
$handlers = $this->getServiceAuthenticatorsFromRequest($service, $request);
|
||||||
|
|
||||||
|
$link = $this->Link('logout');
|
||||||
|
array_walk(
|
||||||
|
$handlers,
|
||||||
|
function (Authenticator &$auth, $name) use ($link) {
|
||||||
|
$auth = $auth->getLogoutHandler(Controller::join_links($link, $name));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->delegateToMultipleHandlers(
|
||||||
|
$handlers,
|
||||||
|
_t('Security.LOGOUT', 'Log out'),
|
||||||
|
$this->getTemplatesFor('logout'),
|
||||||
|
[$this, 'aggregateTabbedForms']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authenticators for the given service, optionally filtered by the ID parameter
|
||||||
|
* of the current request
|
||||||
|
*
|
||||||
|
* @param int $service
|
||||||
|
* @param HTTPRequest $request
|
||||||
|
* @throws HTTPResponse_Exception
|
||||||
|
*/
|
||||||
|
protected function getServiceAuthenticatorsFromRequest($service, HTTPRequest $request)
|
||||||
|
{
|
||||||
|
$authName = null;
|
||||||
|
|
||||||
|
if ($request->param('ID')) {
|
||||||
|
$authName = $request->param('ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to a single named handler - e.g. Security/login/<authname>/
|
||||||
|
if ($authName && $this->hasAuthenticator($authName)) {
|
||||||
|
if ($request) {
|
||||||
|
$request->shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
$authenticator = $this->getAuthenticator($authName);
|
||||||
|
|
||||||
|
if (!$authenticator->supportedServices() & $service) {
|
||||||
|
// Try to be helpful and show the service constant name, e.g. Authenticator::LOGIN
|
||||||
|
$constants = array_flip((new ReflectionClass(Authenticator::class))->getConstants());
|
||||||
|
|
||||||
|
$message = 'Invalid Authenticator "' . $authName . '" for ';
|
||||||
|
if (array_key_exists($service, $constants)) {
|
||||||
|
$message .= 'service: Authenticator::' . $constants[$service];
|
||||||
|
} else {
|
||||||
|
$message .= 'unknown authenticator service';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HTTPResponse_Exception($message, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$handlers = [$authName => $authenticator];
|
||||||
|
} else {
|
||||||
|
// Delegate to all of them, building a tabbed view - e.g. Security/login/
|
||||||
|
$handlers = $this->getApplicableAuthenticators($service);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate tabbed forms from each handler to fragments ready to be rendered.
|
||||||
|
*
|
||||||
|
* @param array $results
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function aggregateTabbedForms(array $results)
|
||||||
|
{
|
||||||
|
$forms = [];
|
||||||
|
foreach ($results as $authName => $singleResult) {
|
||||||
|
// The result *must* be an array with a Form key
|
||||||
|
if (!is_array($singleResult) || !isset($singleResult['Form'])) {
|
||||||
|
user_error('Authenticator "' . $authName . '" doesn\'t support tabbed forms', E_USER_WARNING);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$forms[] = $singleResult['Form'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$forms) {
|
||||||
|
throw new \LogicException('No authenticators found compatible with tabbed forms');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Forms' => ArrayList::create($forms),
|
||||||
|
'Form' => $this->generateTabbedFormSet($forms)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate to a number of handlers and aggregate the results. This is used, for example, to
|
||||||
|
* build the log-in page where there are multiple authenticators active.
|
||||||
*
|
*
|
||||||
* If a single handler is passed, delegateToHandler() will be called instead
|
* If a single handler is passed, delegateToHandler() will be called instead
|
||||||
*
|
*
|
||||||
* @param array|RequestHandler[] $handlers
|
* @param array|RequestHandler[] $handlers
|
||||||
* @param string $title The title of the form
|
* @param string $title The title of the form
|
||||||
* @param array $templates
|
* @param array $templates
|
||||||
|
* @param callable $aggregator
|
||||||
* @return array|HTTPResponse|RequestHandler|DBHTMLText|string
|
* @return array|HTTPResponse|RequestHandler|DBHTMLText|string
|
||||||
*/
|
*/
|
||||||
protected function delegateToMultipleHandlers(array $handlers, $title, array $templates)
|
protected function delegateToMultipleHandlers(array $handlers, $title, array $templates, callable $aggregator)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Simpler case for a single authenticator
|
// Simpler case for a single authenticator
|
||||||
@ -757,30 +803,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
$handlers
|
$handlers
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aggregate all their forms, assuming they all return
|
$fragments = call_user_func_array($aggregator, [$results]);
|
||||||
$forms = [];
|
return $this->renderWrappedController($title, $fragments, $templates);
|
||||||
foreach ($results as $authName => $singleResult) {
|
|
||||||
// The result *must* be an array with a Form key
|
|
||||||
if (!is_array($singleResult) || !isset($singleResult['Form'])) {
|
|
||||||
user_error('Authenticator "' . $authName . '" doesn\'t support a tabbed login', E_USER_WARNING);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$forms[] = $singleResult['Form'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$forms) {
|
|
||||||
throw new \LogicException('No authenticators found compatible with a tabbed login');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->renderWrappedController(
|
|
||||||
$title,
|
|
||||||
[
|
|
||||||
'Forms' => ArrayList::create($forms),
|
|
||||||
'Form' => $this->generateLoginFormSet($forms),
|
|
||||||
],
|
|
||||||
$templates
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -796,8 +820,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
{
|
{
|
||||||
$result = $handler->handleRequest($this->getRequest(), DataModel::inst());
|
$result = $handler->handleRequest($this->getRequest(), DataModel::inst());
|
||||||
|
|
||||||
// Return the customised controller - used to render in a Form
|
// Return the customised controller - may be used to render a Form (e.g. login form)
|
||||||
// Post requests are expected to be login posts, so they'll be handled downstairs
|
|
||||||
if (is_array($result)) {
|
if (is_array($result)) {
|
||||||
$result = $this->renderWrappedController($title, $result, $templates);
|
$result = $this->renderWrappedController($title, $result, $templates);
|
||||||
}
|
}
|
||||||
@ -807,6 +830,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the given fragments into a security page controller with the given title.
|
* Render the given fragments into a security page controller with the given title.
|
||||||
|
*
|
||||||
* @param string $title string The title to give the security page
|
* @param string $title string The title to give the security page
|
||||||
* @param array $fragments A map of objects to render into the page, e.g. "Form"
|
* @param array $fragments A map of objects to render into the page, e.g. "Form"
|
||||||
* @param array $templates An array of templates to use for the render
|
* @param array $templates An array of templates to use for the render
|
||||||
@ -823,10 +847,10 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
|
|
||||||
// Handle any form messages from validation, etc.
|
// Handle any form messages from validation, etc.
|
||||||
$messageType = '';
|
$messageType = '';
|
||||||
$message = $this->getLoginMessage($messageType);
|
$message = $this->getSessionMessage($messageType);
|
||||||
|
|
||||||
// We've displayed the message in the form output, so reset it for the next run.
|
// We've displayed the message in the form output, so reset it for the next run.
|
||||||
static::clearLoginMessage();
|
static::clearSessionMessage();
|
||||||
|
|
||||||
if ($message) {
|
if ($message) {
|
||||||
$messageResult = [
|
$messageResult = [
|
||||||
@ -865,7 +889,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
return $this->delegateToMultipleHandlers(
|
return $this->delegateToMultipleHandlers(
|
||||||
$handlers,
|
$handlers,
|
||||||
_t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
|
_t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
|
||||||
$this->getTemplatesFor('lostpassword')
|
$this->getTemplatesFor('lostpassword'),
|
||||||
|
[$this, 'aggregateTabbedForms']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -893,7 +918,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
return $this->delegateToMultipleHandlers(
|
return $this->delegateToMultipleHandlers(
|
||||||
$handlers,
|
$handlers,
|
||||||
_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'),
|
_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'),
|
||||||
$this->getTemplatesFor('changepassword')
|
$this->getTemplatesFor('changepassword'),
|
||||||
|
[$this, 'aggregateTabbedForms']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1220,7 +1246,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
*/
|
*/
|
||||||
public static function logout_url()
|
public static function logout_url()
|
||||||
{
|
{
|
||||||
return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
|
$logoutUrl = Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
|
||||||
|
return SecurityToken::inst()->addToUrl($logoutUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,3 +17,11 @@ Feature: Log in
|
|||||||
# disable automatic redirection so we can use the profiler
|
# disable automatic redirection so we can use the profiler
|
||||||
When I go to "/admin/"
|
When I go to "/admin/"
|
||||||
And I should see a log-in form
|
And I should see a log-in form
|
||||||
|
|
||||||
|
Scenario: Logout without token
|
||||||
|
Given I am logged in with "ADMIN" permissions
|
||||||
|
When I go to "/Security/logout"
|
||||||
|
Then I should see a log-out form
|
||||||
|
When I press the "Log out" button
|
||||||
|
And I go to "/admin/"
|
||||||
|
Then I should see a log-in form
|
||||||
|
@ -12,6 +12,7 @@ use SilverStripe\Security\LoginAttempt;
|
|||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\Security\SecurityToken;
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use SilverStripe\Dev\FunctionalTest;
|
use SilverStripe\Dev\FunctionalTest;
|
||||||
@ -277,6 +278,60 @@ class SecurityTest extends FunctionalTest
|
|||||||
$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
|
$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLogout()
|
||||||
|
{
|
||||||
|
/* Enable SecurityToken */
|
||||||
|
$securityTokenWasEnabled = SecurityToken::is_enabled();
|
||||||
|
SecurityToken::enable();
|
||||||
|
|
||||||
|
$member = DataObject::get_one(Member::class);
|
||||||
|
|
||||||
|
/* Log in with any user that we can find */
|
||||||
|
Security::setCurrentUser($member);
|
||||||
|
|
||||||
|
/* Visit the Security/logout page with a test referer, but without a security token */
|
||||||
|
$response = $this->get(
|
||||||
|
Config::inst()->get(Security::class, 'logout_url'),
|
||||||
|
null,
|
||||||
|
['Referer' => Director::absoluteBaseURL() . 'testpage']
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Make sure the user is still logged in */
|
||||||
|
$this->assertNotNull(Security::getCurrentUser(), 'User is still logged in.');
|
||||||
|
|
||||||
|
$token = $this->cssParser()->getBySelector('#LogoutForm_Form #LogoutForm_Form_SecurityID');
|
||||||
|
$actions = $this->cssParser()->getBySelector('#LogoutForm_Form input.action');
|
||||||
|
|
||||||
|
/* We have a security token, and an action to allow the user to log out */
|
||||||
|
$this->assertCount(1, $token, 'There is a hidden field containing a security token.');
|
||||||
|
$this->assertCount(1, $actions, 'There is 1 action, allowing the user to log out.');
|
||||||
|
|
||||||
|
/* Submit the form, using the logout action */
|
||||||
|
$response = $this->submitForm(
|
||||||
|
'LogoutForm_Form',
|
||||||
|
null,
|
||||||
|
array(
|
||||||
|
'action_doLogout' => 1,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* We get a good response */
|
||||||
|
$this->assertEquals(302, $response->getStatusCode());
|
||||||
|
$this->assertRegExp(
|
||||||
|
'/testpage/',
|
||||||
|
$response->getHeader('Location'),
|
||||||
|
"Logout form redirects to back to referer."
|
||||||
|
);
|
||||||
|
|
||||||
|
/* User is logged out successfully */
|
||||||
|
$this->assertNull(Security::getCurrentUser(), 'User is logged out.');
|
||||||
|
|
||||||
|
/* Re-disable SecurityToken */
|
||||||
|
if (!$securityTokenWasEnabled) {
|
||||||
|
SecurityToken::disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testExternalBackUrlRedirectionDisallowed()
|
public function testExternalBackUrlRedirectionDisallowed()
|
||||||
{
|
{
|
||||||
// Test internal relative redirect
|
// Test internal relative redirect
|
||||||
|
Loading…
Reference in New Issue
Block a user