Skip to content

Commit

Permalink
FIX Revoke a single session
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Apr 14, 2021
1 parent f70cb2c commit 43c5ced
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 26 deletions.
13 changes: 0 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,6 @@ It is also compatible with the [Silverstripe MFA module suite](https://github.co

## Configuration

### Logout across devices

This module respects the `SilverStripe\Security\RememberLoginHash.logout_across_devices` config setting, which defaults to `true`. This means that the default behaviour is to revoke _all_ a user’s sessions when they log out.

To change this so that logging out will only revoke the session for that one device, use the following config setting:

```yml
SilverStripe\Security\RememberLoginHash:
logout_across_devices: false
```
**Important:** do not set this value to false if users do not have access to the CMS (or a custom UI where they can revoke sessions). Doing so would make it impossible to a user to revoke a session if they suspect their device has been compromised.
### Session timeout

Non-persisted login sessions (those where the user hasn’t ticked “remember me”) should expire after a period of inactivity, so that they’re removed from the list of active sessions even if the user closes their browser without completing the “log out” action. The length of time before expiry matches the `SilverStripe\Control\Session.timeout` value if one is set, otherwise falling back to a default of one hour. This default can be changed via the following config setting:
Expand Down
9 changes: 7 additions & 2 deletions src/Middleware/LoginSessionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Middleware\HTTPMiddleware;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Connect\DatabaseException;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
use SilverStripe\SessionManager\Model\LoginSession;
use SilverStripe\SessionManager\Security\LogInAuthenticationHandler;
Expand All @@ -35,8 +37,11 @@ public function process(HTTPRequest $request, callable $delegate)
// If the session has already been revoked, or we've got a mismatched
// member / session, log the user out (this also revokes the session)
if (!$loginSession || (int)$loginSession->MemberID !== (int)$member->ID) {
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logOut($request);
Config::withConfig(function ($config) use ($request) {
$config->set(RememberLoginHash::class, 'logout_across_devices', false);
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logOut($request);
});
return $delegate($request);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Model/LoginSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public function handlePermission(string $funcName, $member): bool
}

// Members can manage their own sessions
if ($this->ID == $member->ID) {
if ($this->MemberID === $member->ID) {
return true;
}

Expand Down
14 changes: 4 additions & 10 deletions src/Security/LogOutAuthenticationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,10 @@ public function logOut(HTTPRequest $request = null)
$loginHandler = Injector::inst()->get(LogInAuthenticationHandler::class);
$member = Security::getCurrentUser();

if (RememberLoginHash::config()->get('logout_across_devices')) {
foreach ($member->LoginSessions() as $session) {
$session->delete();
}
} else {
$loginSessionID = $request->getSession()->get($loginHandler->getSessionVariable());
$loginSession = LoginSession::get()->byID($loginSessionID);
if ($loginSession && $loginSession->canDelete($member)) {
$loginSession->delete();
}
$loginSessionID = $request->getSession()->get($loginHandler->getSessionVariable());
$loginSession = LoginSession::get()->byID($loginSessionID);
if ($loginSession && $loginSession->canDelete($member)) {
$loginSession->delete();
}

$request->getSession()->clear($loginHandler->getSessionVariable());
Expand Down
63 changes: 63 additions & 0 deletions tests/php/Control/LoginSessionMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace SilverStripe\SessionManager\Tests\Control;

use SilverStripe\Control\Cookie;
use SilverStripe\Control\Middleware\ConfirmationMiddleware\Url;
use SilverStripe\Control\Session;
use SilverStripe\Control\Tests\HttpRequestMockBuilder;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
use SilverStripe\SessionManager\Control\LoginSessionMiddleware;
use SilverStripe\SessionManager\Model\LoginSession;
Expand Down Expand Up @@ -124,4 +127,64 @@ function () use (&$next) {
"Middleware logs user out if session has mismatched member"
);
}

/**
* Assert the RememberLoginHash for un-revoked LoginSessions are untouched
*/
public function testOtherDeviceRememberLoginHashUntouched()
{
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'member_rmh');
Security::setCurrentUser($member);

$session1 = $this->objFromFixture(LoginSession::class, 'rmh1');
$hash1 = RememberLoginHash::generate($member);
$hash1->LoginSessionID = $session1->ID;
$hash1->DeviceID = 'IE2';
$hash1->write();

$session2 = $this->objFromFixture(LoginSession::class, 'rmh2');
$hash2 = RememberLoginHash::generate($member);
$hash2->LoginSessionID = $session2->ID;
$hash2->DeviceID = 'C64';
$hash2->write();

$deviceFilter = ['DeviceID' => ['IE2', 'C64']];

$this->assertSame(2, RememberLoginHash::get()->filter($deviceFilter)->count());
$this->assertSame(
[
'Internet Explorer 2',
'Commodore 64 browser'
],
LoginSession::get()
->filter(['ID' => RememberLoginHash::get()->filter($deviceFilter)->column('LoginSessionID')])
->column('UserAgent')
);

// revoke the 2nd device
$member->LoginSessions()->find('UserAgent', 'Commodore 64 browser')->delete();

// "press f5 to refresh" the 2nd device which will trigger the middleware to call IdentityStore logOut()
$default = Cookie::get('alc_device');
Cookie::set('alc_device', 'C64');
$session = new Session(['activeLoginSession' => $session2->ID]);
$request = $this->buildRequestMock('/', [], [], null, $session);
$request->method('getIP')->willReturn('192.168.0.1');
$middleware = new LoginSessionMiddleware();
$middleware->process($request, function () {
// noop
});
Cookie::set('alc_device', $default);

$this->assertSame(1, RememberLoginHash::get()->filter($deviceFilter)->count());
$this->assertSame(
[
'Internet Explorer 2'
],
LoginSession::get()
->filter(['ID' => RememberLoginHash::get()->filter($deviceFilter)->column('LoginSessionID')])
->column('UserAgent')
);
}
}
13 changes: 13 additions & 0 deletions tests/php/Control/LoginSessionMiddlewareTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ SilverStripe\Security\Member:
member2:
FirstName: 'Garion'
Email: '[email protected]'
member_rmh:
FirstName: 'Bob'
Email: '[email protected]'

SilverStripe\SessionManager\Model\LoginSession:
x1:
LastAccessed: '2003-02-15 10:00:00'
IPAddress: '192.168.0.1'
Member: =>SilverStripe\Security\Member.member1
rmh1:
LastAccessed: '2003-02-15 10:00:00'
IPAddress: '192.168.0.1'
UserAgent: 'Internet Explorer 2'
Member: =>SilverStripe\Security\Member.member_rmh
rmh2:
LastAccessed: '2003-02-15 10:00:00'
IPAddress: '192.168.0.1'
UserAgent: 'Commodore 64 browser'
Member: =>SilverStripe\Security\Member.member_rmh

0 comments on commit 43c5ced

Please sign in to comment.