API Security.remember_username to disable login form autocompletion

This commit is contained in:
Damian Mooyman 2014-04-09 11:41:25 +12:00
parent ee487e28f1
commit 997077ae83
6 changed files with 61 additions and 1 deletions

View File

@ -39,6 +39,8 @@ class FunctionalTest extends SapphireTest {
/** /**
* CSSContentParser for the most recently requested page. * CSSContentParser for the most recently requested page.
*
* @var CSSContentParser
*/ */
protected $cssParser = null; protected $cssParser = null;
@ -176,6 +178,8 @@ class FunctionalTest extends SapphireTest {
/** /**
* Return a CSSContentParser for the most recent content. * Return a CSSContentParser for the most recent content.
*
* @return CSSContentParser
*/ */
public function cssParser() { public function cssParser() {
if(!$this->cssParser) $this->cssParser = new CSSContentParser($this->mainSession->lastContent()); if(!$this->cssParser) $this->cssParser = new CSSContentParser($this->mainSession->lastContent());

View File

@ -0,0 +1,9 @@
# 3.1.5
## Upgrading
* If running an application in an environment where user security is critical, it may be necessary to
assign the config value `Security.remember_username` to false. This will disable persistence of
user login name between sessions, and disable browser auto-completion on the username field.
Note that users of certain browsers who have previously autofilled and saved login credentials
will need to clear their password autofill history before this setting is properly respected.

View File

@ -445,6 +445,7 @@ In addition, you can tighten password security with the following configuration
the user is blocked from further attempts for the timespan defined in `$lock_out_delay_mins` the user is blocked from further attempts for the timespan defined in `$lock_out_delay_mins`
* `Member.lock_out_delay_mins`: Minutes of enforced lockout after incorrect password attempts. * `Member.lock_out_delay_mins`: Minutes of enforced lockout after incorrect password attempts.
Only applies if `lock_out_after_incorrect_logins` is greater than 0. Only applies if `lock_out_after_incorrect_logins` is greater than 0.
* `Security.remember_username`: Set to false to disable autocomplete on login form
## Clickjacking: Prevent iframe Inclusion ## Clickjacking: Prevent iframe Inclusion

View File

@ -80,9 +80,16 @@ class MemberLoginForm extends LoginForm {
new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this), new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this),
// Regardless of what the unique identifer field is (usually 'Email'), it will be held in the // Regardless of what the unique identifer field is (usually 'Email'), it will be held in the
// 'Email' value, below: // 'Email' value, below:
new TextField("Email", $label, Session::get('SessionForms.MemberLoginForm.Email'), null, $this), $emailField = new TextField("Email", $label, null, null, $this),
new PasswordField("Password", _t('Member.PASSWORD', 'Password')) new PasswordField("Password", _t('Member.PASSWORD', 'Password'))
); );
if(Security::config()->remember_username) {
$emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email'));
} else {
// Some browsers won't respect this attribute unless it's added to the form
$this->setAttribute('autocomplete', 'off');
$emailField->setAttribute('autocomplete', 'off');
}
if(Security::config()->autologin_enabled) { if(Security::config()->autologin_enabled) {
$fields->push(new CheckboxField( $fields->push(new CheckboxField(
"Remember", "Remember",

View File

@ -63,6 +63,15 @@ class Security extends Controller {
*/ */
private static $autologin_enabled = true; private static $autologin_enabled = true;
/**
* Determine if login username may be remembered between login sessions
* If set to false this will disable autocomplete and prevent username persisting in the session
*
* @config
* @var bool
*/
private static $remember_username = true;
/** /**
* Location of word list to use for generating passwords * Location of word list to use for generating passwords
* *

View File

@ -16,6 +16,8 @@ class SecurityTest extends FunctionalTest {
protected $priorUniqueIdentifierField = null; protected $priorUniqueIdentifierField = null;
protected $priorRememberUsername = null;
public function setUp() { public function setUp() {
// This test assumes that MemberAuthenticator is present and the default // This test assumes that MemberAuthenticator is present and the default
$this->priorAuthenticators = Authenticator::get_authenticators(); $this->priorAuthenticators = Authenticator::get_authenticators();
@ -29,6 +31,7 @@ class SecurityTest extends FunctionalTest {
// And that the unique identified field is 'Email' // And that the unique identified field is 'Email'
$this->priorUniqueIdentifierField = Member::config()->unique_identifier_field; $this->priorUniqueIdentifierField = Member::config()->unique_identifier_field;
$this->priorRememberUsername = Security::config()->remember_username;
Member::config()->unique_identifier_field = 'Email'; Member::config()->unique_identifier_field = 'Email';
parent::setUp(); parent::setUp();
@ -48,6 +51,7 @@ class SecurityTest extends FunctionalTest {
// Restore unique identifier field // Restore unique identifier field
Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField; Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField;
Security::config()->remember_username = $this->priorRememberUsername;
parent::tearDown(); parent::tearDown();
} }
@ -124,6 +128,32 @@ class SecurityTest extends FunctionalTest {
$this->session()->inst_set('loggedInAs', null); $this->session()->inst_set('loggedInAs', null);
} }
public function testLoginUsernamePersists() {
// Test that username does not persist
$this->session()->inst_set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com');
Security::config()->remember_username = false;
$this->get(Config::inst()->get('Security', 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm #Email input.text');
$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');
$this->assertEquals(1, count($form));
$this->assertEquals('off', (string)$form[0]->attributes()->autocomplete);
// Test that username does persist when necessary
$this->session()->inst_set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com');
Security::config()->remember_username = true;
$this->get(Config::inst()->get('Security', 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm #Email input.text');
$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');
$this->assertEquals(1, count($form));
$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
}
public function testExternalBackUrlRedirectionDisallowed() { public function testExternalBackUrlRedirectionDisallowed() {
// Test internal relative redirect // Test internal relative redirect
$response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'testpage'); $response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'testpage');