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:
Sam Minnee 2017-04-23 15:30:33 +12:00 committed by Simon Erkelens
parent e226b67d06
commit 7af7e6719e
10 changed files with 256 additions and 284 deletions

View File

@ -4,6 +4,5 @@ SilverStripe\Security\MemberAuthenticator\LoginForm:
- Password - Password
SilverStripe\Security\Security: SilverStripe\Security\Security:
default_authenticator: SilverStripe\Security\MemberAuthenticator\Authenticator
authenticators: authenticators:
- SilverStripe\Security\MemberAuthenticator\Authenticator default: SilverStripe\Security\MemberAuthenticator\Authenticator

View File

@ -276,7 +276,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
if (Controller::has_curr()) { if (Controller::has_curr()) {
Controller::curr()->setSession(Session::create(array())); Controller::curr()->setSession(Session::create(array()));
} }
Security::$database_is_ready = null; Security::clear_database_is_ready();
// Set up test routes // Set up test routes
$this->setUpRoutes(); $this->setUpRoutes();

View File

@ -180,7 +180,7 @@ PHP
public function LoginForm() public function LoginForm()
{ {
$authenticator = $this->getAuthenticator(); $authenticator = $this->getAuthenticator('default');
if ($authenticator && $authenticator::supports_cms()) { if ($authenticator && $authenticator::supports_cms()) {
return $authenticator::get_cms_login_form($this); return $authenticator::get_cms_login_form($this);
} }

View File

@ -10,6 +10,7 @@ use InvalidArgumentException;
use SilverStripe\Security\Authenticator as BaseAuthenticator; use SilverStripe\Security\Authenticator as BaseAuthenticator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\LoginAttempt;
/** /**
* Authenticator for the default "member" method * Authenticator for the default "member" method
@ -134,7 +135,7 @@ class Authenticator implements BaseAuthenticator
* @param array $data * @param array $data
* @param Member $member * @param Member $member
*/ */
protected function recordLoginAttempt($data, $member) protected function recordLoginAttempt($data, $member, $success)
{ {
if (!Security::config()->login_recording) { if (!Security::config()->login_recording) {
return; return;

View File

@ -94,21 +94,28 @@ class LoginHandler extends RequestHandler
* @param LoginHandler $formHandler * @param LoginHandler $formHandler
* @return HTTPResponse * @return HTTPResponse
*/ */
public function doLogin($data, $formHandler) public function doLogin($data, $form)
{ {
if ($this->performLogin($data)) { $failureMessage = null;
return $this->logInUserAndRedirect($data, $formHandler);
// Successful login
if ($member = $this->checkLogin($data, $failureMessage)) {
$this->performLogin($member, $data);
return $this->redirectAfterSuccessfulLogin();
} }
$form->sessionMessage($failureMessage, 'bad');
// Failed login
/** @skipUpgrade */ /** @skipUpgrade */
if (array_key_exists('Email', $data)) { if (array_key_exists('Email', $data)) {
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']); Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember'])); Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember']));
} }
return $this->redirectBack();
// Fail to login redirects back to form // 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 * @param array $data
* @return HTTPResponse * @return HTTPResponse
*/ */
protected function logInUserAndRedirect($data, $formHandler) protected function redirectAfterSuccessfulLogin()
{ {
Session::clear('SessionForms.MemberLoginForm.Email'); Session::clear('SessionForms.MemberLoginForm.Email');
Session::clear('SessionForms.MemberLoginForm.Remember'); Session::clear('SessionForms.MemberLoginForm.Remember');
@ -156,13 +163,6 @@ class LoginHandler extends RequestHandler
// Redirect the user to the page where they came from // Redirect the user to the page where they came from
if ($member) { if ($member) {
if (!empty($data['Remember'])) {
Session::set('SessionForms.MemberLoginForm.Remember', '1');
$member->logIn(true);
} else {
$member->logIn();
}
// Welcome message // Welcome message
$message = _t( $message = _t(
'SilverStripe\\Security\\Member.WELCOMEBACK', 'SilverStripe\\Security\\Member.WELCOMEBACK',
@ -188,7 +188,8 @@ class LoginHandler extends RequestHandler
*/ */
public function logout() 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 * @return Member Returns the member object on successful authentication
* or NULL on failure. * or NULL on failure.
*/ */
public function performLogin($data) public function checkLogin($data, &$message)
{ {
$message = null; $message = null;
$member = $this->authenticator->authenticate($data, $message); $member = $this->authenticator->authenticate($data, $message);
if ($member) { if ($member) {
$member->LogIn(isset($data['Remember']));
return $member; return $member;
} else {
Security::setLoginMessage($message, ValidationResult::TYPE_ERROR);
}
// No member, can't login } else {
$this->extend('authenticationFailed', $data); // No member, can't login
return null; $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 * Invoked if password is expired and must be changed
* *

View File

@ -220,10 +220,64 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
protected static $database_is_ready = false; protected static $database_is_ready = false;
/**
* @var array available authenticators
*/
protected static $authenticators = []; protected static $authenticators = [];
/**
* @var string Default authenticator
*/
protected static $default_authenticator = MemberAuthenticator\Authenticator::class; 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 * Get all registered authenticators
* *
@ -231,44 +285,24 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function getAuthenticators() public static function getAuthenticators()
{ {
$authenticatorClasses = self::config()->authenticators; $authenticators = 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);
}
return array_map(function ($class) { return array_map(function ($class) {
return Injector::inst()->get($class); return Injector::inst()->get($class);
}, $authenticatorClasses); }, $authenticators);
} }
/** /**
* Check if a given authenticator is registered * 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 * @return bool Returns TRUE if the authenticator is registered, FALSE
* otherwise. * otherwise.
*/ */
public static function hasAuthenticator($authenticator) public static function hasAuthenticator($authenticator)
{ {
$authenticators = self::config()->get('authenticators'); $authenticators = self::config()->get('authenticators');
if (count($authenticators) === 0) { return !empty($authenticators[$authenticator]);
$authenticators = [self::config()->get('default_authenticator')];
}
return in_array($authenticator, $authenticators, true);
} }
/** /**
@ -358,12 +392,8 @@ class Security extends Controller implements TemplateGlobalProvider
$message = $messageSet['default']; $message = $messageSet['default'];
} }
// Somewhat hackish way to render a login form with an error message. Security::setLoginMessage($message, ValidationResult::TYPE_WARNING);
// $me = new Security(); $loginResponse = (new Security())->login(new HTTPRequest('GET', '/'));
// $form = $me->LoginForm();
// $form->sessionMessage($message, ValidationResult::TYPE_WARNING);
// Session::set('MemberLoginForm.force_message', 1);
$loginResponse = $me->login();
if ($loginResponse instanceof HTTPResponse) { if ($loginResponse instanceof HTTPResponse) {
return $loginResponse; 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 * Get the login form to process according to the submitted data
* *
@ -443,7 +429,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function LoginForm() public function LoginForm()
{ {
$authenticator = $this->getAuthenticator(); $authenticator = $this->getAuthenticator('default');
if ($authenticator) { if ($authenticator) {
$handler = $authenticator->getLoginHandler($this->Link()); $handler = $authenticator->getLoginHandler($this->Link());
return $handler->handleRequest($this->request, DataModel::inst()); return $handler->handleRequest($this->request, DataModel::inst());
@ -654,7 +640,9 @@ class Security extends Controller implements TemplateGlobalProvider
* For multiple authenticators, Security_MultiAuthenticatorLogin is used. * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
* See getTemplatesFor and getIncludeTemplate for how to override template logic * 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) public function login($request)
{ {
@ -666,61 +654,54 @@ class Security extends Controller implements TemplateGlobalProvider
$link = $this->link("login"); $link = $this->link("login");
// Delegate to a single handler - Security/login/<authname>/... // Delegate to a single handler - Security/login/<authname>/...
if ($authenticatorName = $request->param('ID')) { if ($name = $request->param('ID')) {
$request->shift(); $request->shift();
$authenticator = $this->getAuthenticator($authenticatorName); $authenticator = $this->getAuthenticator($name);
if (!$authenticator) { 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)); $authenticators = [ $name => $authenticator ];
return $this->delegateToHandler(
$handler,
_t('Security.LOGIN', 'Log in'),
$this->getTemplatesFor('login')
);
// Delegate to all of them, building a tabbed view - Security/login // Delegate to all of them, building a tabbed view - Security/login
} else { } else {
$handlers = $this->getAuthenticators(); $authenticators = $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')
);
}
} }
$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. * 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. * 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 string $title The title of the form
* @param array $templates * @param array $templates
* @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string * @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 // Process each of the handlers
$results = array_map( $results = array_map(
function ($handler) { 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. * Render the given fragments into a security page controller with the given title.
* @param $title string The title to give the security page * @param string $title string The title to give the security page
* @param $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 $templates An array of templates to use for the render * @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) protected function renderWrappedController($title, array $fragments, array $templates)
{ {
@ -804,7 +786,7 @@ class Security extends Controller implements TemplateGlobalProvider
'Message' => DBField::create_field('HTMLFragment', $message), 'Message' => DBField::create_field('HTMLFragment', $message),
'MessageType' => $messageType 'MessageType' => $messageType
]; ];
$result = array_merge($fragments, $messageResult); $fragments = array_merge($fragments, $messageResult);
} }
return $controller->customise($fragments)->renderWith($templates); return $controller->customise($fragments)->renderWith($templates);
@ -823,7 +805,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function lostpassword() public function lostpassword()
{ {
$handler = $this->getAuthenticator()->getLostPasswordHandler( $handler = $this->getAuthenticator('default')->getLostPasswordHandler(
Controller::join_links($this->link(), 'lostpassword') 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. * Show the "change password" page.
* This page can either be called directly by logged-in users * 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')); 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 * Factory method for the lost password form
* *
@ -1219,6 +1201,46 @@ class Security extends Controller implements TemplateGlobalProvider
return true; 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 * @config
* @var string Set the default login dest * @var string Set the default login dest

View File

@ -30,7 +30,7 @@ class BasicAuthTest extends FunctionalTest
// Fixtures assume Email is the field used to identify the log in identity // Fixtures assume Email is the field used to identify the log in identity
Member::config()->unique_identifier_field = 'Email'; 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; Member::config()->lock_out_after_incorrect_logins = 10;
} }

View File

@ -9,13 +9,14 @@ use SilverStripe\Security\PasswordEncryptor;
use SilverStripe\Security\PasswordEncryptor_PHPHash; use SilverStripe\Security\PasswordEncryptor_PHPHash;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\Authenticator;
use SilverStripe\Security\MemberLoginForm; use SilverStripe\Security\MemberAuthenticator\LoginForm;
use SilverStripe\Security\CMSMemberLoginForm; use SilverStripe\Security\CMSMemberLoginForm;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Control\HTTPRequest;
class MemberAuthenticatorTest extends SapphireTest class MemberAuthenticatorTest extends SapphireTest
{ {
@ -41,59 +42,6 @@ class MemberAuthenticatorTest extends SapphireTest
parent::tearDown(); 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() public function testCustomIdentifierField()
{ {
@ -109,36 +57,46 @@ class MemberAuthenticatorTest extends SapphireTest
public function testGenerateLoginForm() public function testGenerateLoginForm()
{ {
$authenticator = new Authenticator();
$controller = new Security(); $controller = new Security();
// Create basic login form // Create basic login form
$frontendForm = MemberAuthenticator::get_login_form($controller); $frontendResponse = $authenticator
$this->assertTrue($frontendForm instanceof MemberLoginForm); ->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 // Supports cms login form
$this->assertTrue(MemberAuthenticator::supports_cms()); $this->assertTrue(MemberAuthenticator::supports_cms());
$cmsForm = MemberAuthenticator::get_cms_login_form($controller); $cmsForm = MemberAuthenticator::get_cms_login_form($controller);
$this->assertTrue($cmsForm instanceof CMSMemberLoginForm); $this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
} }
*/
/** /**
* Test that a member can be authenticated via their temp id * Test that a member can be authenticated via their temp id
*/ */
public function testAuthenticateByTempID() public function testAuthenticateByTempID()
{ {
$authenticator = new Authenticator();
$member = new Member(); $member = new Member();
$member->Email = 'test1@test.com'; $member->Email = 'test1@test.com';
$member->PasswordEncryption = "sha1"; $member->PasswordEncryption = "sha1";
$member->Password = "mypassword"; $member->Password = "mypassword";
$member->write(); $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 // If the user has never logged in, then the tempid should be empty
$tempID = $member->TempIDHash; $tempID = $member->TempIDHash;
$this->assertEmpty($tempID); $this->assertEmpty($tempID);
@ -149,35 +107,32 @@ class MemberAuthenticatorTest extends SapphireTest
$this->assertNotEmpty($tempID); $this->assertNotEmpty($tempID);
// Test correct login // Test correct login
$result = MemberAuthenticator::authenticate( $result = $authenticator->authenticate(
array( array(
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'mypassword' 'Password' => 'mypassword'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->ID, $member->ID); $this->assertEquals($result->ID, $member->ID);
$this->assertEmpty($form->getMessage()); $this->assertEmpty($message);
// Test incorrect login // Test incorrect login
$form->clearMessage(); $result = $authenticator->authenticate(
$result = MemberAuthenticator::authenticate(
array( array(
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertEmpty($result); $this->assertEmpty($result);
$this->assertEquals( $this->assertEquals(
_t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'), _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() public function testDefaultAdmin()
{ {
// Make form $authenticator = new Authenticator();
$controller = new Security();
/**
* @skipUpgrade
*/
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
// Test correct login // Test correct login
$result = MemberAuthenticator::authenticate( $result = $authenticator->authenticate(
array( array(
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'password' 'Password' => 'password'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->Email, Security::default_admin_username()); $this->assertEquals($result->Email, Security::default_admin_username());
$this->assertEmpty($form->getMessage()); $this->assertEmpty($message);
// Test incorrect login // Test incorrect login
$form->clearMessage(); $result = $authenticator->authenticate(
$result = MemberAuthenticator::authenticate(
array( array(
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertEmpty($result); $this->assertEmpty($result);
$this->assertEquals( $this->assertEquals(
'The provided details don\'t seem to be correct. Please try again.', '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() public function testDefaultAdminLockOut()
{ {
$authenticator = new Authenticator();
Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1); Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
Config::inst()->update(Member::class, 'lock_out_delay_mins', 10); Config::inst()->update(Member::class, 'lock_out_delay_mins', 10);
DBDatetime::set_mock_now('2016-04-18 00:00:00'); 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 // Test correct login
MemberAuthenticator::authenticate( $authenticator->authenticate(
[ [
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'wrongpassword' 'Password' => 'wrongpassword'
], ],
$form $dummy
); );
$this->assertTrue(Member::default_admin()->isLockedOut()); $this->assertTrue(Member::default_admin()->isLockedOut());

View File

@ -237,13 +237,13 @@ class MemberTest extends FunctionalTest
$this->assertNotNull($member); $this->assertNotNull($member);
// Initiate a password-reset // 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); $this->assertEquals($response->getStatusCode(), 302);
// We should get redirected to Security/passwordsent // We should get redirected to Security/passwordsent
$this->assertContains( $this->assertContains(
'Security/passwordsent/testuser@example.com', 'Security/lostpassword/passwordsent/testuser@example.com',
urldecode($response->getHeader('Location')) 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 // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
// should remove all previous hashes for this device // should remove all previous hashes for this device
$response = $this->post( $response = $this->post(
'Security/LoginForm', 'Security/login/default/LoginForm',
array( array(
'Email' => $m1->Email, 'Email' => $m1->Email,
'Password' => '1nitialPassword', 'Password' => '1nitialPassword',
'AuthenticationMethod' => MemberAuthenticator::class, 'action_doLogin' => 'action_doLogin'
'action_dologin' => 'action_dologin'
), ),
null, null,
$this->session(), $this->session(),

View File

@ -187,14 +187,14 @@ class SecurityTest extends FunctionalTest
} }
$response = $this->getRecursive('SecurityTest_SecuredController'); $response = $this->getRecursive('SecurityTest_SecuredController');
$this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody()); $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 // 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 // No message/context is available as the user has not attempted to view the secured controller
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/'); $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
$this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody()); $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->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 // BackURL with permission error (wrong permissions) should not redirect
$this->logInAs('grouplessmember'); $this->logInAs('grouplessmember');
@ -233,7 +233,7 @@ class SecurityTest extends FunctionalTest
/* View the Security/login page */ /* View the Security/login page */
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $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 */ /* 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.'); $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 */ /* Submit the form, using only the logout action and a hidden field for the authenticator */
$response = $this->submitForm( $response = $this->submitForm(
'MemberLoginForm_LoginForm', 'LoginForm_LoginForm',
null, null,
array( array(
'AuthenticationMethod' => MemberAuthenticator::class, 'action_logout' => 1,
'action_dologout' => 1,
) )
); );
@ -268,7 +267,7 @@ class SecurityTest extends FunctionalTest
/* Attempt to get into the admin section */ /* Attempt to get into the admin section */
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $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 */ /* 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'); $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')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this $items = $this
->cssParser() ->cssParser()
->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email'); ->getBySelector('#LoginForm_LoginForm #LoginForm_LoginForm_Email');
$this->assertEquals(1, count($items)); $this->assertEquals(1, count($items));
$this->assertEmpty((string)$items[0]->attributes()->value); $this->assertEmpty((string)$items[0]->attributes()->value);
$this->assertEquals('off', (string)$items[0]->attributes()->autocomplete); $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(1, count($form));
$this->assertEquals('off', (string)$form[0]->attributes()->autocomplete); $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')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this $items = $this
->cssParser() ->cssParser()
->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email'); ->getBySelector('#LoginForm_LoginForm #LoginForm_LoginForm_Email');
$this->assertEquals(1, count($items)); $this->assertEquals(1, count($items));
$this->assertEquals('myuser@silverstripe.com', (string)$items[0]->attributes()->value); $this->assertEquals('myuser@silverstripe.com', (string)$items[0]->attributes()->value);
$this->assertNotEquals('off', (string)$items[0]->attributes()->autocomplete); $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->assertEquals(1, count($form));
$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete); $this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
} }
@ -436,7 +435,7 @@ class SecurityTest extends FunctionalTest
// Request new password by email // Request new password by email
$response = $this->get('Security/lostpassword'); $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'); $this->assertEmailSent('testuser@example.com');
@ -648,9 +647,7 @@ class SecurityTest extends FunctionalTest
public function testDatabaseIsReadyWithInsufficientMemberColumns() public function testDatabaseIsReadyWithInsufficientMemberColumns()
{ {
$old = Security::$force_database_is_ready; Security::clear_database_is_ready();
Security::$force_database_is_ready = null;
Security::$database_is_ready = false;
DBClassName::clear_classname_cache(); DBClassName::clear_classname_cache();
// Assumption: The database has been built correctly by the test runner, // 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 // Rebuild the database (which re-adds the Email column), and try again
static::resetDBSchema(true); static::resetDBSchema(true);
$this->assertTrue(Security::database_is_ready()); $this->assertTrue(Security::database_is_ready());
Security::$force_database_is_ready = $old;
} }
public function testSecurityControllerSendsRobotsTagHeader() public function testSecurityControllerSendsRobotsTagHeader()
@ -697,13 +692,13 @@ class SecurityTest extends FunctionalTest
$this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
return $this->submitForm( return $this->submitForm(
"MemberLoginForm_LoginForm", "LoginForm_LoginForm",
null, null,
array( array(
'Email' => $email, 'Email' => $email,
'Password' => $password, 'Password' => $password,
'AuthenticationMethod' => MemberAuthenticator::class, 'AuthenticationMethod' => MemberAuthenticator::class,
'action_dologin' => 1, 'action_doLogin' => 1,
) )
); );
} }
@ -751,7 +746,7 @@ class SecurityTest extends FunctionalTest
*/ */
protected function getValidationResult() protected function getValidationResult()
{ {
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result'); $result = $this->session()->inst_get('FormInfo.LoginForm_LoginForm.result');
if ($result) { if ($result) {
return unserialize($result); return unserialize($result);
} }