/** * Deletes existing tokens for this member * if logout_across_devices is true, all tokens are deleted, otherwise * only the token for the provided device ID will be removed * * @param Member $member * @param string $alcDevice */ public static function clear(Member $member, $alcDevice = null) { if (!$member->exists()) { return; } $filter = array('MemberID' => $member->ID); if (!static::config()->logout_across_devices && $alcDevice) { $filter['DeviceID'] = $alcDevice; } RememberLoginHash::get()->filter($filter)->removeAll(); }
public function testRememberMeMultipleDevices() { $m1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'noexpiry'); // First device $m1->login(true); Cookie::set('alc_device', null); // Second device $m1->login(true); // Hash of first device $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First(); $this->assertNotNull($firstHash); // Hash of second device $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->Last(); $this->assertNotNull($secondHash); // DeviceIDs are different $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID); // re-generates the hashes so we can get the tokens $firstHash->Hash = $firstHash->getNewHash($m1); $firstToken = $firstHash->getToken(); $firstHash->write(); $secondHash->Hash = $secondHash->getNewHash($m1); $secondToken = $secondHash->getToken(); $secondHash->write(); // Accessing the login page should show the user's name straight away $response = $this->get('Security/login', $this->session(), null, array('alc_enc' => $m1->ID . ':' . $firstToken, 'alc_device' => $firstHash->DeviceID)); $message = _t('Member.LOGGEDINAS', "You're logged in as {name}.", array('name' => $m1->FirstName)); $this->assertContains($message, $response->getBody()); $this->session()->inst_set('loggedInAs', null); // Accessing the login page from the second device $response = $this->get('Security/login', $this->session(), null, array('alc_enc' => $m1->ID . ':' . $secondToken, 'alc_device' => $secondHash->DeviceID)); $this->assertContains($message, $response->getBody()); $logout_across_devices = Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices'); // Logging out from the second device - only one device being logged out Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', false); $response = $this->get('Security/logout', $this->session(), null, array('alc_enc' => $m1->ID . ':' . $secondToken, 'alc_device' => $secondHash->DeviceID)); $this->assertEquals(RememberLoginHash::get()->filter(array('MemberID' => $m1->ID, 'DeviceID' => $firstHash->DeviceID))->Count(), 1); // Logging out from any device when all login hashes should be removed Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', true); $m1->login(true); $response = $this->get('Security/logout', $this->session()); $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(), 0); Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', $logout_across_devices); }
/** * Logs this member out. */ public function logOut() { $this->extend('beforeMemberLoggedOut'); Session::clear("loggedInAs"); if (Member::config()->login_marker_cookie) { Cookie::set(Member::config()->login_marker_cookie, null, 0); } Session::destroy(); $this->extend('memberLoggedOut'); // Clears any potential previous hashes for this member RememberLoginHash::clear($this, Cookie::get('alc_device')); Cookie::set('alc_enc', null); // // Clear the Remember Me cookie Cookie::force_expiry('alc_enc'); Cookie::set('alc_device', null); Cookie::force_expiry('alc_device'); // Switch back to live in order to avoid infinite loops when // redirecting to the login screen (if this login screen is versioned) Session::clear('readingMode'); $this->write(); // Audit logging hook $this->extend('memberLoggedOut'); }
/** * Constructor * * @skipUpgrade * @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 = Member::singleton()->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)"), RememberLoginHash::config()->get('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'); }