mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API: Security.authenticators is now a map, not an array
Authenticators is now a map of keys -> service names. The key is used in things such as URL segments. The “default_authenticator” value has been replaced with the key “default” in this map, although in time a default authenticator may not be needed. IX: Refactor login() to avoid code duplication on single/multiple handlers IX: Refactor LoginHandler to be more amenable to extension IX: Fixed permissionFailure hack his LoginHandler is expected to be the starting point for other custom authenticators so it should be easier to repurpose components `of it. IX: Fix database-is-ready checks in tests. IX: Fixed MemberAuthenticatorTest to match the new API IX: Update security URLs in MemberTest
This commit is contained in:
parent
e226b67d06
commit
7af7e6719e
@ -4,6 +4,5 @@ SilverStripe\Security\MemberAuthenticator\LoginForm:
|
||||
- Password
|
||||
|
||||
SilverStripe\Security\Security:
|
||||
default_authenticator: SilverStripe\Security\MemberAuthenticator\Authenticator
|
||||
authenticators:
|
||||
- SilverStripe\Security\MemberAuthenticator\Authenticator
|
||||
default: SilverStripe\Security\MemberAuthenticator\Authenticator
|
||||
|
@ -276,7 +276,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
|
||||
if (Controller::has_curr()) {
|
||||
Controller::curr()->setSession(Session::create(array()));
|
||||
}
|
||||
Security::$database_is_ready = null;
|
||||
Security::clear_database_is_ready();
|
||||
|
||||
// Set up test routes
|
||||
$this->setUpRoutes();
|
||||
|
@ -180,7 +180,7 @@ PHP
|
||||
|
||||
public function LoginForm()
|
||||
{
|
||||
$authenticator = $this->getAuthenticator();
|
||||
$authenticator = $this->getAuthenticator('default');
|
||||
if ($authenticator && $authenticator::supports_cms()) {
|
||||
return $authenticator::get_cms_login_form($this);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use InvalidArgumentException;
|
||||
use SilverStripe\Security\Authenticator as BaseAuthenticator;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\LoginAttempt;
|
||||
|
||||
/**
|
||||
* Authenticator for the default "member" method
|
||||
@ -134,7 +135,7 @@ class Authenticator implements BaseAuthenticator
|
||||
* @param array $data
|
||||
* @param Member $member
|
||||
*/
|
||||
protected function recordLoginAttempt($data, $member)
|
||||
protected function recordLoginAttempt($data, $member, $success)
|
||||
{
|
||||
if (!Security::config()->login_recording) {
|
||||
return;
|
||||
|
@ -94,21 +94,28 @@ class LoginHandler extends RequestHandler
|
||||
* @param LoginHandler $formHandler
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
public function doLogin($data, $formHandler)
|
||||
public function doLogin($data, $form)
|
||||
{
|
||||
if ($this->performLogin($data)) {
|
||||
return $this->logInUserAndRedirect($data, $formHandler);
|
||||
$failureMessage = null;
|
||||
|
||||
// Successful login
|
||||
if ($member = $this->checkLogin($data, $failureMessage)) {
|
||||
$this->performLogin($member, $data);
|
||||
return $this->redirectAfterSuccessfulLogin();
|
||||
}
|
||||
|
||||
$form->sessionMessage($failureMessage, 'bad');
|
||||
|
||||
// Failed login
|
||||
|
||||
/** @skipUpgrade */
|
||||
if (array_key_exists('Email', $data)) {
|
||||
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
|
||||
Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember']));
|
||||
}
|
||||
|
||||
return $this->redirectBack();
|
||||
// Fail to login redirects back to form
|
||||
return $formHandler->redirectBackToForm();
|
||||
return $form->getRequestHandler()->redirectBackToForm();
|
||||
}
|
||||
|
||||
|
||||
@ -132,7 +139,7 @@ class LoginHandler extends RequestHandler
|
||||
* @param array $data
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
protected function logInUserAndRedirect($data, $formHandler)
|
||||
protected function redirectAfterSuccessfulLogin()
|
||||
{
|
||||
Session::clear('SessionForms.MemberLoginForm.Email');
|
||||
Session::clear('SessionForms.MemberLoginForm.Remember');
|
||||
@ -156,13 +163,6 @@ class LoginHandler extends RequestHandler
|
||||
|
||||
// Redirect the user to the page where they came from
|
||||
if ($member) {
|
||||
if (!empty($data['Remember'])) {
|
||||
Session::set('SessionForms.MemberLoginForm.Remember', '1');
|
||||
$member->logIn(true);
|
||||
} else {
|
||||
$member->logIn();
|
||||
}
|
||||
|
||||
// Welcome message
|
||||
$message = _t(
|
||||
'SilverStripe\\Security\\Member.WELCOMEBACK',
|
||||
@ -188,7 +188,8 @@ class LoginHandler extends RequestHandler
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
return Security::singleton()->logout();
|
||||
Security::singleton()->logout();
|
||||
return $this->redirectBack();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,22 +199,33 @@ class LoginHandler extends RequestHandler
|
||||
* @return Member Returns the member object on successful authentication
|
||||
* or NULL on failure.
|
||||
*/
|
||||
public function performLogin($data)
|
||||
public function checkLogin($data, &$message)
|
||||
{
|
||||
$message = null;
|
||||
$member = $this->authenticator->authenticate($data, $message);
|
||||
if ($member) {
|
||||
$member->LogIn(isset($data['Remember']));
|
||||
return $member;
|
||||
} else {
|
||||
Security::setLoginMessage($message, ValidationResult::TYPE_ERROR);
|
||||
}
|
||||
|
||||
// No member, can't login
|
||||
$this->extend('authenticationFailed', $data);
|
||||
return null;
|
||||
} else {
|
||||
// No member, can't login
|
||||
$this->extend('authenticationFailed', $data);
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to authenticate the user
|
||||
*
|
||||
* @param array $data Submitted data
|
||||
* @return Member Returns the member object on successful authentication
|
||||
* or NULL on failure.
|
||||
*/
|
||||
public function performLogin($member, $data)
|
||||
{
|
||||
$member->LogIn(isset($data['Remember']));
|
||||
return $member;
|
||||
}
|
||||
/**
|
||||
* Invoked if password is expired and must be changed
|
||||
*
|
||||
|
@ -220,10 +220,64 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
protected static $database_is_ready = false;
|
||||
|
||||
/**
|
||||
* @var array available authenticators
|
||||
*/
|
||||
protected static $authenticators = [];
|
||||
|
||||
/**
|
||||
* @var string Default authenticator
|
||||
*/
|
||||
protected static $default_authenticator = MemberAuthenticator\Authenticator::class;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
|
||||
$frameOptions = $this->config()->get('frame_options');
|
||||
if ($frameOptions) {
|
||||
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
|
||||
}
|
||||
|
||||
// Prevent search engines from indexing the login page
|
||||
$robotsTag = $this->config()->get('robots_tag');
|
||||
if ($robotsTag) {
|
||||
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return $this->httpError(404); // no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected authenticator for this request
|
||||
*
|
||||
* @param $name string The identifier of the authenticator in your config
|
||||
* @return string Class name of Authenticator
|
||||
* @throws LogicException
|
||||
*/
|
||||
protected function getAuthenticator($name)
|
||||
{
|
||||
$authenticators = self::config()->authenticators;
|
||||
|
||||
if (!$name) $name = 'default';
|
||||
|
||||
if (isset($authenticators[$name])) {
|
||||
return Injector::inst()->get($authenticators[$name]);
|
||||
}
|
||||
|
||||
throw new LogicException('No valid authenticator found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered authenticators
|
||||
*
|
||||
@ -231,44 +285,24 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public static function getAuthenticators()
|
||||
{
|
||||
$authenticatorClasses = self::config()->authenticators;
|
||||
$default = self::config()->default_authenticator;
|
||||
|
||||
if (!$authenticatorClasses) {
|
||||
if ($default) {
|
||||
$authenticatorClasses = [$default];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// put default authenticator first (mainly for tab-order on loginform)
|
||||
// But only if there's no other authenticator
|
||||
if (($key = array_search($default, $authenticatorClasses, true)) && count($$authenticatorClasses) > 1) {
|
||||
unset($authenticatorClasses[$key]);
|
||||
array_unshift($authenticatorClasses, $default);
|
||||
}
|
||||
$authenticators = self::config()->authenticators;
|
||||
|
||||
return array_map(function ($class) {
|
||||
return Injector::inst()->get($class);
|
||||
}, $authenticatorClasses);
|
||||
}, $authenticators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given authenticator is registered
|
||||
*
|
||||
* @param string $authenticator Name of the authenticator class to check
|
||||
* @param string $authenticator The configured identifier of the authenicator
|
||||
* @return bool Returns TRUE if the authenticator is registered, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public static function hasAuthenticator($authenticator)
|
||||
{
|
||||
$authenticators = self::config()->get('authenticators');
|
||||
if (count($authenticators) === 0) {
|
||||
$authenticators = [self::config()->get('default_authenticator')];
|
||||
}
|
||||
|
||||
return in_array($authenticator, $authenticators, true);
|
||||
return !empty($authenticators[$authenticator]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -358,12 +392,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$message = $messageSet['default'];
|
||||
}
|
||||
|
||||
// Somewhat hackish way to render a login form with an error message.
|
||||
// $me = new Security();
|
||||
// $form = $me->LoginForm();
|
||||
// $form->sessionMessage($message, ValidationResult::TYPE_WARNING);
|
||||
// Session::set('MemberLoginForm.force_message', 1);
|
||||
$loginResponse = $me->login();
|
||||
Security::setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
||||
$loginResponse = (new Security())->login(new HTTPRequest('GET', '/'));
|
||||
if ($loginResponse instanceof HTTPResponse) {
|
||||
return $loginResponse;
|
||||
}
|
||||
@ -391,50 +421,6 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
));
|
||||
}
|
||||
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
|
||||
$frameOptions = $this->config()->get('frame_options');
|
||||
if ($frameOptions) {
|
||||
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
|
||||
}
|
||||
|
||||
// Prevent search engines from indexing the login page
|
||||
$robotsTag = $this->config()->get('robots_tag');
|
||||
if ($robotsTag) {
|
||||
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->httpError(404); // no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected authenticator for this request
|
||||
*
|
||||
* @return string Class name of Authenticator
|
||||
* @throws LogicException
|
||||
*/
|
||||
protected function getAuthenticator()
|
||||
{
|
||||
$authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
|
||||
if ($authenticator && self::hasAuthenticator($authenticator)) {
|
||||
return Injector::inst()->get($authenticator);
|
||||
|
||||
} elseif ($authenticator !== '') {
|
||||
$authenticators = self::getAuthenticators();
|
||||
if (count($authenticators) > 0) {
|
||||
return $authenticators[0];
|
||||
}
|
||||
}
|
||||
|
||||
throw new LogicException('No valid authenticator found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login form to process according to the submitted data
|
||||
*
|
||||
@ -443,7 +429,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public function LoginForm()
|
||||
{
|
||||
$authenticator = $this->getAuthenticator();
|
||||
$authenticator = $this->getAuthenticator('default');
|
||||
if ($authenticator) {
|
||||
$handler = $authenticator->getLoginHandler($this->Link());
|
||||
return $handler->handleRequest($this->request, DataModel::inst());
|
||||
@ -654,7 +640,9 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
* For multiple authenticators, Security_MultiAuthenticatorLogin is used.
|
||||
* See getTemplatesFor and getIncludeTemplate for how to override template logic
|
||||
*
|
||||
* @return string|HTTPResponse Returns the "login" page as HTML code.
|
||||
* @param $request
|
||||
* @return HTTPResponse|string Returns the "login" page as HTML code.
|
||||
* @throws HTTPResponse_Exception
|
||||
*/
|
||||
public function login($request)
|
||||
{
|
||||
@ -666,61 +654,54 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$link = $this->link("login");
|
||||
|
||||
// Delegate to a single handler - Security/login/<authname>/...
|
||||
if ($authenticatorName = $request->param('ID')) {
|
||||
if ($name = $request->param('ID')) {
|
||||
$request->shift();
|
||||
|
||||
$authenticator = $this->getAuthenticator($authenticatorName);
|
||||
$authenticator = $this->getAuthenticator($name);
|
||||
if (!$authenticator) {
|
||||
throw new HTTPResponse_Exception(404, 'No authenticator "' . $authenticatorName . '"');
|
||||
throw new HTTPResponse_Exception(404, 'No authenticator "' . $name . '"');
|
||||
}
|
||||
|
||||
$handler = $authenticator->getLoginHandler(Controller::join_links($link, $authenticatorName));
|
||||
|
||||
return $this->delegateToHandler(
|
||||
$handler,
|
||||
_t('Security.LOGIN', 'Log in'),
|
||||
$this->getTemplatesFor('login')
|
||||
);
|
||||
$authenticators = [ $name => $authenticator ];
|
||||
|
||||
// Delegate to all of them, building a tabbed view - Security/login
|
||||
} else {
|
||||
$handlers = $this->getAuthenticators();
|
||||
array_walk(
|
||||
$handlers,
|
||||
function (&$auth, $name) use ($link) {
|
||||
$auth = $auth->getLoginHandler(Controller::join_links($link, $name));
|
||||
}
|
||||
);
|
||||
|
||||
if (count($handlers) === 1) {
|
||||
return $this->delegateToHandler(
|
||||
array_values($handlers)[0],
|
||||
_t('Security.LOGIN', 'Log in'),
|
||||
$this->getTemplatesFor('login')
|
||||
);
|
||||
|
||||
} else {
|
||||
return $this->delegateToFormSet(
|
||||
$handlers,
|
||||
_t('Security.LOGIN', 'Log in'),
|
||||
$this->getTemplatesFor('login')
|
||||
);
|
||||
}
|
||||
$authenticators = $this->getAuthenticators();
|
||||
}
|
||||
|
||||
$handlers = $authenticators;
|
||||
array_walk(
|
||||
$handlers,
|
||||
function (&$auth, $name) use ($link) {
|
||||
$auth = $auth->getLoginHandler(Controller::join_links($link, $name));
|
||||
}
|
||||
);
|
||||
|
||||
return $this->delegateToMultipleHandlers(
|
||||
$handlers,
|
||||
_t('Security.LOGIN', 'Log in'),
|
||||
$this->getTemplatesFor('login')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
|
||||
* This is used to built the log-in page where there are multiple authenticators active.
|
||||
*
|
||||
* If a single handler is passed, delegateToHandler() will be called instead
|
||||
*
|
||||
* @param string $title The title of the form
|
||||
* @param array $templates
|
||||
* @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
|
||||
*/
|
||||
protected function delegateToFormSet(array $handlers, $title, array $templates)
|
||||
protected function delegateToMultipleHandlers(array $handlers, $title, array $templates)
|
||||
{
|
||||
|
||||
// Simpler case for a single authenticator
|
||||
if (count($handlers) === 1) {
|
||||
return $this->delegateToHandler(array_values($handlers)[0], $title, $templates);
|
||||
}
|
||||
|
||||
// Process each of the handlers
|
||||
$results = array_map(
|
||||
function ($handler) {
|
||||
@ -778,9 +759,10 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
|
||||
/**
|
||||
* Render the given fragments into a security page controller with the given title.
|
||||
* @param $title string The title to give the security page
|
||||
* @param $fragments A map of objects to render into the page, e.g. "Form"
|
||||
* @param $templates An array of templates to use for the render
|
||||
* @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 $templates An array of templates to use for the render
|
||||
* @return HTTPResponse|\SilverStripe\ORM\FieldType\DBHTMLText
|
||||
*/
|
||||
protected function renderWrappedController($title, array $fragments, array $templates)
|
||||
{
|
||||
@ -804,7 +786,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
'Message' => DBField::create_field('HTMLFragment', $message),
|
||||
'MessageType' => $messageType
|
||||
];
|
||||
$result = array_merge($fragments, $messageResult);
|
||||
$fragments = array_merge($fragments, $messageResult);
|
||||
}
|
||||
|
||||
return $controller->customise($fragments)->renderWith($templates);
|
||||
@ -823,7 +805,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public function lostpassword()
|
||||
{
|
||||
$handler = $this->getAuthenticator()->getLostPasswordHandler(
|
||||
$handler = $this->getAuthenticator('default')->getLostPasswordHandler(
|
||||
Controller::join_links($this->link(), 'lostpassword')
|
||||
);
|
||||
|
||||
@ -834,26 +816,6 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link to the password reset form.
|
||||
*
|
||||
* GET parameters used:
|
||||
* - m: member ID
|
||||
* - t: plaintext token
|
||||
*
|
||||
* @param Member $member Member object associated with this link.
|
||||
* @param string $autologinToken The auto login token.
|
||||
* @return string
|
||||
*/
|
||||
public static function getPasswordResetLink($member, $autologinToken)
|
||||
{
|
||||
$autologinToken = urldecode($autologinToken);
|
||||
$selfControllerClass = __CLASS__;
|
||||
/** @var static $selfController */
|
||||
$selfController = new $selfControllerClass();
|
||||
return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the "change password" page.
|
||||
* This page can either be called directly by logged-in users
|
||||
@ -941,6 +903,26 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link to the password reset form.
|
||||
*
|
||||
* GET parameters used:
|
||||
* - m: member ID
|
||||
* - t: plaintext token
|
||||
*
|
||||
* @param Member $member Member object associated with this link.
|
||||
* @param string $autologinToken The auto login token.
|
||||
* @return string
|
||||
*/
|
||||
public static function getPasswordResetLink($member, $autologinToken)
|
||||
{
|
||||
$autologinToken = urldecode($autologinToken);
|
||||
$selfControllerClass = __CLASS__;
|
||||
/** @var static $selfController */
|
||||
$selfController = new $selfControllerClass();
|
||||
return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for the lost password form
|
||||
*
|
||||
@ -1219,6 +1201,46 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the database_is_ready cache
|
||||
*/
|
||||
public static function clear_database_is_ready()
|
||||
{
|
||||
self::$database_is_ready = null;
|
||||
self::$force_database_is_ready = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the database_is_ready call to return a certain value - used for testing
|
||||
*/
|
||||
public static function force_database_is_ready($isReady)
|
||||
{
|
||||
self::$force_database_is_ready = $isReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable recording of login attempts
|
||||
* through the {@link LoginRecord} object.
|
||||
*
|
||||
* @deprecated 4.0 Use the "Security.login_recording" config setting instead
|
||||
* @param boolean $bool
|
||||
*/
|
||||
public static function set_login_recording($bool)
|
||||
{
|
||||
Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
|
||||
self::$login_recording = (bool)$bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Use the "Security.login_recording" config setting instead
|
||||
* @return boolean
|
||||
*/
|
||||
public static function login_recording()
|
||||
{
|
||||
Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
|
||||
return self::$login_recording;
|
||||
}
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var string Set the default login dest
|
||||
|
@ -30,7 +30,7 @@ class BasicAuthTest extends FunctionalTest
|
||||
|
||||
// Fixtures assume Email is the field used to identify the log in identity
|
||||
Member::config()->unique_identifier_field = 'Email';
|
||||
Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test
|
||||
Security::force_database_is_ready(true); // Prevents Member test subclasses breaking ready test
|
||||
Member::config()->lock_out_after_incorrect_logins = 10;
|
||||
}
|
||||
|
||||
|
@ -9,13 +9,14 @@ use SilverStripe\Security\PasswordEncryptor;
|
||||
use SilverStripe\Security\PasswordEncryptor_PHPHash;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\MemberAuthenticator;
|
||||
use SilverStripe\Security\MemberLoginForm;
|
||||
use SilverStripe\Security\MemberAuthenticator\Authenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\LoginForm;
|
||||
use SilverStripe\Security\CMSMemberLoginForm;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
|
||||
class MemberAuthenticatorTest extends SapphireTest
|
||||
{
|
||||
@ -41,59 +42,6 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testLegacyPasswordHashMigrationUponLogin()
|
||||
{
|
||||
$member = new Member();
|
||||
|
||||
$field=Member::config()->unique_identifier_field;
|
||||
|
||||
$member->$field = 'test1@test.com';
|
||||
$member->PasswordEncryption = "sha1";
|
||||
$member->Password = "mypassword";
|
||||
$member->write();
|
||||
|
||||
$data = array(
|
||||
'Email' => $member->$field,
|
||||
'Password' => 'mypassword'
|
||||
);
|
||||
MemberAuthenticator::authenticate($data);
|
||||
|
||||
/**
|
||||
* @var Member $member
|
||||
*/
|
||||
$member = DataObject::get_by_id(Member::class, $member->ID);
|
||||
$this->assertEquals($member->PasswordEncryption, "sha1_v2.4");
|
||||
$result = $member->checkPassword('mypassword');
|
||||
$this->assertTrue($result->isValid());
|
||||
}
|
||||
|
||||
public function testNoLegacyPasswordHashMigrationOnIncompatibleAlgorithm()
|
||||
{
|
||||
Config::inst()->update(
|
||||
PasswordEncryptor::class,
|
||||
'encryptors',
|
||||
array('crc32' => array(PasswordEncryptor_PHPHash::class => 'crc32'))
|
||||
);
|
||||
$field=Member::config()->unique_identifier_field;
|
||||
|
||||
$member = new Member();
|
||||
$member->$field = 'test2@test.com';
|
||||
$member->PasswordEncryption = "crc32";
|
||||
$member->Password = "mypassword";
|
||||
$member->write();
|
||||
|
||||
$data = array(
|
||||
'Email' => $member->$field,
|
||||
'Password' => 'mypassword'
|
||||
);
|
||||
MemberAuthenticator::authenticate($data);
|
||||
|
||||
$member = DataObject::get_by_id(Member::class, $member->ID);
|
||||
$this->assertEquals($member->PasswordEncryption, "crc32");
|
||||
$result = $member->checkPassword('mypassword');
|
||||
$this->assertTrue($result->isValid());
|
||||
}
|
||||
|
||||
public function testCustomIdentifierField()
|
||||
{
|
||||
|
||||
@ -109,36 +57,46 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
|
||||
public function testGenerateLoginForm()
|
||||
{
|
||||
$authenticator = new Authenticator();
|
||||
|
||||
$controller = new Security();
|
||||
|
||||
// Create basic login form
|
||||
$frontendForm = MemberAuthenticator::get_login_form($controller);
|
||||
$this->assertTrue($frontendForm instanceof MemberLoginForm);
|
||||
$frontendResponse = $authenticator
|
||||
->getLoginHandler($controller->link())
|
||||
->handleRequest(new HTTPRequest('get', '/'), \SilverStripe\ORM\DataModel::inst());
|
||||
|
||||
$this->assertTrue(is_array($frontendResponse));
|
||||
$this->assertTrue(isset($frontendResponse['Form']));
|
||||
$this->assertTrue($frontendResponse['Form'] instanceof LoginForm);
|
||||
}
|
||||
|
||||
/* TO DO - reenable
|
||||
public function testGenerateCMSLoginForm()
|
||||
{
|
||||
$authenticator = new Authenticator();
|
||||
|
||||
// Supports cms login form
|
||||
$this->assertTrue(MemberAuthenticator::supports_cms());
|
||||
$cmsForm = MemberAuthenticator::get_cms_login_form($controller);
|
||||
$this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Test that a member can be authenticated via their temp id
|
||||
*/
|
||||
public function testAuthenticateByTempID()
|
||||
{
|
||||
$authenticator = new Authenticator();
|
||||
|
||||
$member = new Member();
|
||||
$member->Email = 'test1@test.com';
|
||||
$member->PasswordEncryption = "sha1";
|
||||
$member->Password = "mypassword";
|
||||
$member->write();
|
||||
|
||||
// Make form
|
||||
$controller = new Security();
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
|
||||
|
||||
// If the user has never logged in, then the tempid should be empty
|
||||
$tempID = $member->TempIDHash;
|
||||
$this->assertEmpty($tempID);
|
||||
@ -149,35 +107,32 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
$this->assertNotEmpty($tempID);
|
||||
|
||||
// Test correct login
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'tempid' => $tempID,
|
||||
'Password' => 'mypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals($result->ID, $member->ID);
|
||||
$this->assertEmpty($form->getMessage());
|
||||
$this->assertEmpty($message);
|
||||
|
||||
// Test incorrect login
|
||||
$form->clearMessage();
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'tempid' => $tempID,
|
||||
'Password' => 'notmypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
|
||||
$this->assertEmpty($result);
|
||||
$this->assertEquals(
|
||||
_t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
|
||||
$form->getMessage()
|
||||
$message
|
||||
);
|
||||
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
|
||||
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,61 +140,50 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
*/
|
||||
public function testDefaultAdmin()
|
||||
{
|
||||
// Make form
|
||||
$controller = new Security();
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
|
||||
$authenticator = new Authenticator();
|
||||
|
||||
// Test correct login
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'Email' => 'admin',
|
||||
'Password' => 'password'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals($result->Email, Security::default_admin_username());
|
||||
$this->assertEmpty($form->getMessage());
|
||||
$this->assertEmpty($message);
|
||||
|
||||
// Test incorrect login
|
||||
$form->clearMessage();
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'Email' => 'admin',
|
||||
'Password' => 'notmypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
$this->assertEmpty($result);
|
||||
$this->assertEquals(
|
||||
'The provided details don\'t seem to be correct. Please try again.',
|
||||
$form->getMessage()
|
||||
$message
|
||||
);
|
||||
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
|
||||
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
|
||||
}
|
||||
|
||||
public function testDefaultAdminLockOut()
|
||||
{
|
||||
$authenticator = new Authenticator();
|
||||
|
||||
Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
|
||||
Config::inst()->update(Member::class, 'lock_out_delay_mins', 10);
|
||||
DBDatetime::set_mock_now('2016-04-18 00:00:00');
|
||||
$controller = new Security();
|
||||
/** @skipUpgrade */
|
||||
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
|
||||
|
||||
// Test correct login
|
||||
MemberAuthenticator::authenticate(
|
||||
$authenticator->authenticate(
|
||||
[
|
||||
'Email' => 'admin',
|
||||
'Password' => 'wrongpassword'
|
||||
],
|
||||
$form
|
||||
$dummy
|
||||
);
|
||||
|
||||
$this->assertTrue(Member::default_admin()->isLockedOut());
|
||||
|
@ -237,13 +237,13 @@ class MemberTest extends FunctionalTest
|
||||
$this->assertNotNull($member);
|
||||
|
||||
// Initiate a password-reset
|
||||
$response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email));
|
||||
$response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => $member->Email));
|
||||
|
||||
$this->assertEquals($response->getStatusCode(), 302);
|
||||
|
||||
// We should get redirected to Security/passwordsent
|
||||
$this->assertContains(
|
||||
'Security/passwordsent/testuser@example.com',
|
||||
'Security/lostpassword/passwordsent/testuser@example.com',
|
||||
urldecode($response->getHeader('Location'))
|
||||
);
|
||||
|
||||
@ -942,12 +942,11 @@ class MemberTest extends FunctionalTest
|
||||
// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
|
||||
// should remove all previous hashes for this device
|
||||
$response = $this->post(
|
||||
'Security/LoginForm',
|
||||
'Security/login/default/LoginForm',
|
||||
array(
|
||||
'Email' => $m1->Email,
|
||||
'Password' => '1nitialPassword',
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologin' => 'action_dologin'
|
||||
'action_doLogin' => 'action_doLogin'
|
||||
),
|
||||
null,
|
||||
$this->session(),
|
||||
|
@ -187,14 +187,14 @@ class SecurityTest extends FunctionalTest
|
||||
}
|
||||
$response = $this->getRecursive('SecurityTest_SecuredController');
|
||||
$this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody());
|
||||
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
|
||||
$this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
|
||||
|
||||
// Non-logged in user should not be redirected, but instead shown the login form
|
||||
// No message/context is available as the user has not attempted to view the secured controller
|
||||
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
|
||||
$this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody());
|
||||
$this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
|
||||
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
|
||||
$this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
|
||||
|
||||
// BackURL with permission error (wrong permissions) should not redirect
|
||||
$this->logInAs('grouplessmember');
|
||||
@ -233,7 +233,7 @@ class SecurityTest extends FunctionalTest
|
||||
/* View the Security/login page */
|
||||
$response = $this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
|
||||
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
|
||||
$items = $this->cssParser()->getBySelector('#LoginForm_LoginForm input.action');
|
||||
|
||||
/* We have only 1 input, one to allow the user to log in as someone else */
|
||||
$this->assertEquals(count($items), 1, 'There is 1 input, allowing the user to log in as someone else.');
|
||||
@ -242,11 +242,10 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
/* Submit the form, using only the logout action and a hidden field for the authenticator */
|
||||
$response = $this->submitForm(
|
||||
'MemberLoginForm_LoginForm',
|
||||
'LoginForm_LoginForm',
|
||||
null,
|
||||
array(
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologout' => 1,
|
||||
'action_logout' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
@ -268,7 +267,7 @@ class SecurityTest extends FunctionalTest
|
||||
/* Attempt to get into the admin section */
|
||||
$response = $this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
|
||||
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
|
||||
$items = $this->cssParser()->getBySelector('#LoginForm_LoginForm input.text');
|
||||
|
||||
/* We have 2 text inputs - one for email, and another for the password */
|
||||
$this->assertEquals(count($items), 2, 'There are 2 inputs - one for email, another for password');
|
||||
@ -287,11 +286,11 @@ class SecurityTest extends FunctionalTest
|
||||
$this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
$items = $this
|
||||
->cssParser()
|
||||
->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
|
||||
->getBySelector('#LoginForm_LoginForm #LoginForm_LoginForm_Email');
|
||||
$this->assertEquals(1, count($items));
|
||||
$this->assertEmpty((string)$items[0]->attributes()->value);
|
||||
$this->assertEquals('off', (string)$items[0]->attributes()->autocomplete);
|
||||
$form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
|
||||
$form = $this->cssParser()->getBySelector('#LoginForm_LoginForm');
|
||||
$this->assertEquals(1, count($form));
|
||||
$this->assertEquals('off', (string)$form[0]->attributes()->autocomplete);
|
||||
|
||||
@ -301,11 +300,11 @@ class SecurityTest extends FunctionalTest
|
||||
$this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
$items = $this
|
||||
->cssParser()
|
||||
->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
|
||||
->getBySelector('#LoginForm_LoginForm #LoginForm_LoginForm_Email');
|
||||
$this->assertEquals(1, count($items));
|
||||
$this->assertEquals('myuser@silverstripe.com', (string)$items[0]->attributes()->value);
|
||||
$this->assertNotEquals('off', (string)$items[0]->attributes()->autocomplete);
|
||||
$form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
|
||||
$form = $this->cssParser()->getBySelector('#LoginForm_LoginForm');
|
||||
$this->assertEquals(1, count($form));
|
||||
$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
|
||||
}
|
||||
@ -436,7 +435,7 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
// Request new password by email
|
||||
$response = $this->get('Security/lostpassword');
|
||||
$response = $this->post('Security/LostPasswordForm', array('Email' => 'testuser@example.com'));
|
||||
$response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => 'testuser@example.com'));
|
||||
|
||||
$this->assertEmailSent('testuser@example.com');
|
||||
|
||||
@ -648,9 +647,7 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
public function testDatabaseIsReadyWithInsufficientMemberColumns()
|
||||
{
|
||||
$old = Security::$force_database_is_ready;
|
||||
Security::$force_database_is_ready = null;
|
||||
Security::$database_is_ready = false;
|
||||
Security::clear_database_is_ready();
|
||||
DBClassName::clear_classname_cache();
|
||||
|
||||
// Assumption: The database has been built correctly by the test runner,
|
||||
@ -666,8 +663,6 @@ class SecurityTest extends FunctionalTest
|
||||
// Rebuild the database (which re-adds the Email column), and try again
|
||||
static::resetDBSchema(true);
|
||||
$this->assertTrue(Security::database_is_ready());
|
||||
|
||||
Security::$force_database_is_ready = $old;
|
||||
}
|
||||
|
||||
public function testSecurityControllerSendsRobotsTagHeader()
|
||||
@ -697,13 +692,13 @@ class SecurityTest extends FunctionalTest
|
||||
$this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
|
||||
return $this->submitForm(
|
||||
"MemberLoginForm_LoginForm",
|
||||
"LoginForm_LoginForm",
|
||||
null,
|
||||
array(
|
||||
'Email' => $email,
|
||||
'Password' => $password,
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologin' => 1,
|
||||
'action_doLogin' => 1,
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -751,7 +746,7 @@ class SecurityTest extends FunctionalTest
|
||||
*/
|
||||
protected function getValidationResult()
|
||||
{
|
||||
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result');
|
||||
$result = $this->session()->inst_get('FormInfo.LoginForm_LoginForm.result');
|
||||
if ($result) {
|
||||
return unserialize($result);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user