/** * Check if the submitted member data is valid (server-side) * * Check if a member with that email doesn't already exist, or if it does * that it is this member. * * @param array $data Submitted data * @return bool Returns TRUE if the submitted data is valid, otherwise * FALSE. */ public function php($data) { $valid = parent::php($data); $identifierField = (string) Member::config()->unique_identifier_field; // Only validate identifier field if it's actually set. This could be the case if // somebody removes `Email` from the list of required fields. if (isset($data[$identifierField])) { $id = isset($data['ID']) ? (int) $data['ID'] : 0; if (!$id && ($ctrl = $this->form->getController())) { // get the record when within GridField (Member editing page in CMS) if ($ctrl instanceof GridFieldDetailForm_ItemRequest && ($record = $ctrl->getRecord())) { $id = $record->ID; } } // If there's no ID passed via controller or form-data, use the assigned member (if available) if (!$id && ($member = $this->getForMember())) { $id = $member->exists() ? $member->ID : 0; } // set the found ID to the data array, so that extensions can also use it $data['ID'] = $id; $members = Member::get()->filter($identifierField, $data[$identifierField]); if ($id) { $members = $members->exclude('ID', $id); } if ($members->count() > 0) { $this->validationError($identifierField, _t('Member.VALIDATIONMEMBEREXISTS', 'A member already exists with the same {identifier}', array('identifier' => Member::singleton()->fieldLabel($identifierField))), 'required'); $valid = false; } } // Execute the validators on the extensions $results = $this->extend('updatePHP', $data, $this->form); $results[] = $valid; return min($results); }
public function testCustomIdentifierField() { $origField = Member::config()->unique_identifier_field; Member::config()->unique_identifier_field = 'Username'; $label = singleton('SilverStripe\\Security\\Member')->fieldLabel(Member::config()->unique_identifier_field); $this->assertEquals($label, 'Username'); Member::config()->unique_identifier_field = $origField; }
/** * Attempt to find and authenticate member if possible from the given data * * @param array $data * @param Form $form * @param bool &$success Success flag * @return Member Found member, regardless of successful login */ protected static function authenticate_member($data, $form, &$success) { // Default success to false $success = false; // Attempt to identify by temporary ID $member = null; $email = null; if (!empty($data['tempid'])) { // Find user by tempid, in case they are re-validating an existing session $member = Member::member_from_tempid($data['tempid']); if ($member) { $email = $member->Email; } } // Otherwise, get email from posted value instead /** @skipUpgrade */ if (!$member && !empty($data['Email'])) { $email = $data['Email']; } // Check default login (see Security::setDefaultAdmin()) $asDefaultAdmin = $email === Security::default_admin_username(); if ($asDefaultAdmin) { // If logging is as default admin, ensure record is setup correctly $member = Member::default_admin(); $success = !$member->isLockedOut() && Security::check_default_admin($email, $data['Password']); //protect against failed login if ($success) { return $member; } } // Attempt to identify user by email if (!$member && $email) { // Find user by email $member = Member::get()->filter(Member::config()->unique_identifier_field, $email)->first(); } // Validate against member if possible if ($member && !$asDefaultAdmin) { $result = $member->checkPassword($data['Password']); $success = $result->valid(); } else { $result = new ValidationResult(false, _t('Member.ERRORWRONGCRED')); } // Emit failure to member and form (if available) if (!$success) { if ($member) { $member->registerFailedLogin(); } if ($form) { $form->sessionMessage($result->message(), 'bad'); } } else { if ($member) { $member->registerSuccessfulLogin(); } } return $member; }
public function setUp() { parent::setUp(); // 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 Member::config()->lock_out_after_incorrect_logins = 10; }
public function testExtendedPermissionsStopEditingOwnProfile() { $existingExtensions = Member::config()->get('extensions'); Member::config()->update('extensions', array('CMSProfileControllerTestExtension')); $member = $this->objFromFixture('SilverStripe\\Security\\Member', 'user1'); $this->session()->inst_set('loggedInAs', $member->ID); $response = $this->post('admin/myprofile/EditForm', array('action_save' => 1, 'ID' => $member->ID, 'FirstName' => 'JoeEdited', 'Surname' => 'BloggsEdited', 'Email' => $member->Email, 'Locale' => $member->Locale, 'Password[_Password]' => 'password', 'Password[_ConfirmPassword]' => 'password')); $member = $this->objFromFixture('SilverStripe\\Security\\Member', 'user1'); $this->assertNotEquals($member->FirstName, 'JoeEdited', 'FirstName field was NOT changed because we modified canEdit'); Member::config()->remove('extensions')->update('extensions', $existingExtensions); }
/** * Constructor * * @param Controller $controller The parent controller, necessary to * create the appropriate form action tag. * @param string $name The method on the controller that will return this * form object. * @param FieldList $fields All of the fields in the form - a * {@link FieldList} of {@link FormField} * objects. * @param FieldList|FormAction $actions All of the action buttons in the * form - a {@link FieldList} of * {@link FormAction} objects * @param bool $checkCurrentUser If set to TRUE, it will be checked if a * the user is currently logged in, and if * so, only a logout button will be rendered */ public function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true) { // This is now set on the class directly to make it easier to create subclasses // $this->authenticator_class = $authenticatorClassName; $customCSS = project() . '/css/member_login.css'; if (Director::fileExists($customCSS)) { Requirements::css($customCSS); } if (isset($_REQUEST['BackURL'])) { $backURL = $_REQUEST['BackURL']; } else { $backURL = Session::get('BackURL'); } if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) { $fields = FieldList::create(HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this)); $actions = FieldList::create(FormAction::create("logout", _t('Member.BUTTONLOGINOTHER', "Log in as someone else"))); } else { if (!$fields) { $label = singleton('SilverStripe\\Security\\Member')->fieldLabel(Member::config()->unique_identifier_field); $fields = FieldList::create(HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this), $emailField = TextField::create("Email", $label, null, null, $this), PasswordField::create("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) { $fields->push(CheckboxField::create("Remember", _t('Member.KEEPMESIGNEDIN', "Keep me signed in"))->setAttribute('title', sprintf(_t('Member.REMEMBERME', "Remember me next time? (for %d days on this device)"), Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'token_expiry_days')))); } } if (!$actions) { $actions = FieldList::create(FormAction::create('dologin', _t('Member.BUTTONLOGIN', "Log in")), LiteralField::create('forgotPassword', '<p id="ForgotPassword"><a href="' . Security::lost_password_url() . '">' . _t('Member.BUTTONLOSTPASSWORD', "I've lost my password") . '</a></p>')); } } if (isset($backURL)) { $fields->push(HiddenField::create('BackURL', 'BackURL', $backURL)); } // Reduce attack surface by enforcing POST requests $this->setFormMethod('POST', true); parent::__construct($controller, $name, $fields, $actions); $this->setValidator(RequiredFields::create('Email', 'Password')); // Focus on the email input when the page is loaded $js = <<<JS \t\t\t(function() { \t\t\t\tvar el = document.getElementById("MemberLoginForm_LoginForm_Email"); \t\t\t\tif(el && el.focus && (typeof jQuery == 'undefined' || jQuery(el).is(':visible'))) el.focus(); \t\t\t})(); JS; Requirements::customScript($js, 'MemberLoginFormFieldFocus'); }
public function testAlternatingRepeatedLoginAttempts() { Member::config()->lock_out_after_incorrect_logins = 3; // ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member1 = DataObject::get_by_id("SilverStripe\\Security\\Member", $this->idFromFixture('SilverStripe\\Security\\Member', 'test')); $member2 = DataObject::get_by_id("SilverStripe\\Security\\Member", $this->idFromFixture('SilverStripe\\Security\\Member', 'noexpiry')); $this->assertNull($member1->LockedOutUntil); $this->assertNull($member2->LockedOutUntil); // BUT, DOING AN ADDITIONAL LOG-IN WITH EITHER OF THEM WILL LOCK OUT, SINCE THAT IS THE 3RD FAILURE IN // THIS SESSION $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member1 = DataObject::get_by_id("SilverStripe\\Security\\Member", $this->idFromFixture('SilverStripe\\Security\\Member', 'test')); $this->assertNotNull($member1->LockedOutUntil); $this->doTestLoginForm('*****@*****.**', 'incorrectpassword'); $member2 = DataObject::get_by_id("SilverStripe\\Security\\Member", $this->idFromFixture('SilverStripe\\Security\\Member', 'noexpiry')); $this->assertNotNull($member2->LockedOutUntil); }
/** * Test that the PasswordExpiry date is set when passwords are changed */ public function testPasswordExpirySetting() { Member::config()->password_expiry_days = 90; $member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test'); $this->assertNotNull($member); $valid = $member->changePassword("Xx?1234234"); $this->assertTrue($valid->valid()); $expiryDate = date('Y-m-d', time() + 90 * 86400); $this->assertEquals($expiryDate, $member->PasswordExpiry); Member::config()->password_expiry_days = null; $valid = $member->changePassword("Xx?1234235"); $this->assertTrue($valid->valid()); $this->assertNull($member->PasswordExpiry); }
/** * Event handler called before writing to the database. */ public function onBeforeWrite() { if ($this->SetPassword) { $this->Password = $this->SetPassword; } // If a member with the same "unique identifier" already exists with a different ID, don't allow merging. // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), // but rather a last line of defense against data inconsistencies. $identifierField = Member::config()->unique_identifier_field; if ($this->{$identifierField}) { // Note: Same logic as Member_Validator class $filter = array("\"{$identifierField}\"" => $this->{$identifierField}); if ($this->ID) { $filter[] = array('"Member"."ID" <> ?' => $this->ID); } $existingRecord = DataObject::get_one('SilverStripe\\Security\\Member', $filter); if ($existingRecord) { throw new ValidationException(ValidationResult::create(false, _t('Member.ValidationIdentifierFailed', 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', 'Values in brackets show "fieldname = value", usually denoting an existing email address', array('id' => $existingRecord->ID, 'name' => $identifierField, 'value' => $this->{$identifierField})))); } } // We don't send emails out on dev/tests sites to prevent accidentally spamming users. // However, if TestMailer is in use this isn't a risk. if ((Director::isLive() || Email::mailer() instanceof TestMailer) && $this->isChanged('Password') && $this->record['Password'] && $this->config()->notify_password_change) { /** @var Email $e */ $e = Email::create(); $e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')); $e->setTemplate('ChangePasswordEmail'); $e->populateTemplate($this); $e->setTo($this->Email); $e->send(); } // The test on $this->ID is used for when records are initially created. // Note that this only works with cleartext passwords, as we can't rehash // existing passwords. if (!$this->ID && $this->Password || $this->isChanged('Password')) { //reset salt so that it gets regenerated - this will invalidate any persistant login cookies // or other information encrypted with this Member's settings (see self::encryptWithUserSettings) $this->Salt = ''; // Password was changed: encrypt the password according the settings $encryption_details = Security::encrypt_password($this->Password, $this->Salt, $this->PasswordEncryption ? $this->PasswordEncryption : Security::config()->password_encryption_algorithm, $this); // Overwrite the Password property with the hashed value $this->Password = $encryption_details['password']; $this->Salt = $encryption_details['salt']; $this->PasswordEncryption = $encryption_details['algorithm']; // If we haven't manually set a password expiry if (!$this->isChanged('PasswordExpiry')) { // then set it for us if (self::config()->password_expiry_days) { $this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days); } else { $this->PasswordExpiry = null; } } } // save locale if (!$this->Locale) { $this->Locale = i18n::get_locale(); } parent::onBeforeWrite(); }
public function testFailedLoginCount() { $maxFailedLoginsAllowed = 3; //set up the config variables to enable login lockouts Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed); $member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test'); $failedLoginCount = $member->FailedLoginCount; for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) { $member->registerFailedLogin(); $this->assertEquals(++$failedLoginCount, $member->FailedLoginCount, 'Failed to increment $member->FailedLoginCount'); $this->assertFalse($member->isLockedOut(), "Member has been locked out too early"); } }