NEW: Add AuthenticationHandler interface

NEW: Add IdentityStore for registering log-in / log-out data
NEW: Add AuthenticationRequestFilter for managing login
NEW: Add Security:setCurrentUser() / Security::getCurrentUser()
NEW: Add FunctionalTest::logOut()
This commit is contained in:
Sam Minnee 2017-05-08 07:11:00 +12:00 committed by Simon Erkelens
parent c4194f0ed2
commit f9ea752bae
15 changed files with 663 additions and 205 deletions

View File

@ -8,5 +8,26 @@ SilverStripe\Security\Security:
default: SilverStripe\Security\MemberAuthenticator\Authenticator
cms: SilverStripe\Security\MemberAuthenticator\CMSAuthenticator
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\RequestProcessor:
properties:
filters:
- '%$SilverStripe\Security\AuthenticationRequestFilter'
SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler:
properties:
SessionVariable: loggedInAs
SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler:
properties:
TokenCookieName: alc_enc
DeviceCookieName: alc_device
CascadeLogInTo: %$SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler
SilverStripe\Security\IdentityStore:
class: SilverStripe\Security\AuthenticationRequestFilter
SilverStripe\Security\AuthenticationRequestFilter:
handlers:
session: SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler
alc: SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler
SilverStripe\Security\MemberAuthenticator\CMSSecurity:
reauth_enabled: true
reauth_enabled: true

View File

@ -7,6 +7,7 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken;
use SilverStripe\View\SSViewer;
use PHPUnit_Framework_AssertionFailedError;
@ -104,6 +105,9 @@ class FunctionalTest extends SapphireTest
// basis.
BasicAuth::protect_entire_site(false);
$this->session()->inst_clear('loggedInAs');
Security::setCurrentUser(null);
SecurityToken::disable();
}
@ -401,15 +405,26 @@ class FunctionalTest extends SapphireTest
*/
public function logInAs($member)
{
if (is_object($member)) {
$memberID = $member->ID;
} elseif (is_numeric($member)) {
$memberID = $member;
} else {
$memberID = $this->idFromFixture('SilverStripe\\Security\\Member', $member);
if (is_numeric($member)) {
$member = DataObject::get_by_id(Member::class, $member);
} elseif (!is_object($member)) {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', $member);
}
$this->session()->inst_set('loggedInAs', $memberID);
$this->session()->inst_set('loggedInAs', $member->ID);
Security::setCurrentUser($member);
}
/**
* Log in as the given member
*
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
*/
public function logOut()
{
$this->session()->inst_set('loggedInAs', null);
Security::setCurrentUser(null);
}
/**

View File

@ -1250,7 +1250,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$this->cache_generatedMembers[$permCode] = $member;
}
$member->logIn();
Security::setCurrentUser($member);
return $member->ID;
}

View File

@ -0,0 +1,42 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Member;
/**
* An AuthenticationHandler is responsible for providing an identity (in the form of a Member object) for
* a given HTTPRequest.
*
* It should return the authenticated Member if successful. If a Member cannot be found from the current
* request it should *not* attempt to redirect the visitor to a log-in from or 3rd party handler, as that
* is the responsibiltiy of other systems.
*/
interface AuthenticationHandler
{
/**
* Given the current request, authenticate the request for non-session authorization (outside the CMS).
*
* The Member returned from this method will be provided to the Manager for use in the OperationResolver context
* in place of the current CMS member.
*
* Authenticators can be given a priority. In this case, the authenticator with the highest priority will be
* returned first. If not provided, it will default to a low number.
*
* An example for configuring the BasicAuthAuthenticator:
*
* <code>
* SilverStripe\Security\Security:
* authentication_handlers:
* - SilverStripe\Security\BasicAuthentionHandler
* </code>
*
* @param HTTPRequest $request The current HTTP request
* @return Member|null The authenticated Member, or null if this auth mechanism isn't used.
* @throws ValidationException If authentication data exists but does not match a member.
*/
public function authenticateRequest(HTTPRequest $request);
}

View File

@ -0,0 +1,96 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\ORM\DataModel;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
class AuthenticationRequestFilter implements RequestFilter, IdentityStore
{
use Configurable;
protected function getHandlers()
{
return array_map(
function ($identifier) {
return Injector::inst()->get($identifier);
},
$this->config()->get('handlers')
);
}
/**
* Identify the current user from the request
*/
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{
try {
foreach ($this->getHandlers() as $handler) {
// @todo Update requestfilter logic to allow modification of initial response
// in order to add cookies, etc
$member = $handler->authenticateRequest($request, new HTTPResponse());
if ($member) {
// @todo Remove the static coupling here
Security::setCurrentUser($member);
break;
}
}
} catch (ValidationException $e) {
throw new HTTPResponse_Exception(
"Bad log-in details: " . $e->getMessage(),
400
);
}
}
/**
* No-op
*/
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{
}
/**
* Log into the identity-store handlers attached to this request filter
*
* @inherit
*/
public function logIn(Member $member, $persistent, HTTPRequest $request)
{
// @todo Coupling here isn't ideal.
$member->beforeMemberLoggedIn();
foreach ($this->getHandlers() as $handler) {
if ($handler instanceof IdentityStore) {
$handler->logIn($member, $persistent, $request);
}
}
// @todo Coupling here isn't ideal.
Security::setCurrentUser($member);
$member->afterMemberLoggedIn();
}
/**
* Log out of all the identity-store handlers attached to this request filter
*
* @inherit
*/
public function logOut(HTTPRequest $request)
{
foreach ($this->getHandlers() as $handler) {
if ($handler instanceof IdentityStore) {
$handler->logOut($request);
}
}
// @todo Coupling here isn't ideal.
Security::setCurrentUser(null);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/**
* Represents an authentication handler that can have identities logged into & out of it.
* For example, SessionAuthenticationHandler is an IdentityStore (as we can write a new member to it)
* but BasicAuthAuthenticationHandler is not (as it's up to the browser to handle log-in / log-out)
*/
interface IdentityStore
{
/**
* Log the given member into this identity store.
*
* @param $member The member to log in.
* @param $persistent boolean If set to true, the login may persist beyond the current session.
* @param $request The request of the visitor that is logging in, to get, for example, cookies.
* @param $response The response object to modify, if needed.
*/
public function logIn(Member $member, $persistent, HTTPRequest $request);
/**
* Log any logged-in member out of this identity store.
*
* @param $request The request of the visitor that is logging out, to get, for example, cookies.
* @param $response The response object to modify, if needed.
*/
public function logOut(HTTPRequest $request);
}

View File

@ -390,33 +390,6 @@ class Member extends DataObject implements TemplateGlobalProvider
return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp();
}
/**
* Regenerate the session_id.
* This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
* They have caused problems in certain
* quirky problems (such as using the Windmill 0.3.6 proxy).
*/
public static function session_regenerate_id()
{
if (!self::config()->session_regenerate_id) {
return;
}
// This can be called via CLI during testing.
if (Director::is_cli()) {
return;
}
$file = '';
$line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function!
if (!headers_sent($file, $line)) {
@session_regenerate_id(true);
}
}
/**
* Set a {@link PasswordValidator} object to use to validate member's passwords.
*
@ -447,52 +420,34 @@ class Member extends DataObject implements TemplateGlobalProvider
}
/**
* Logs this member in
* @deprecated Use Security::setCurrentUser() or IdentityStore::logIn()
*
* @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
*/
public function logIn($remember = false)
public function logIn()
{
user_error("This method is deprecated and now only logs in for the current request", E_USER_WARNING);
Security::setCurrentUser($this);
}
/**
* Called before a member is logged in via session/cookie/etc
*/
public function beforeMemberLoggedIn()
{
// @todo Move to middleware on the AuthenticationRequestFilter IdentityStore
$this->extend('beforeMemberLoggedIn');
}
self::session_regenerate_id();
Session::set("loggedInAs", $this->ID);
// This lets apache rules detect whether the user has logged in
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0);
}
if (Security::config()->autologin_enabled) {
// Cleans up any potential previous hash for this member on this device
if ($alcDevice = Cookie::get('alc_device')) {
RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
}
if ($remember) {
$rememberLoginHash = RememberLoginHash::generate($this);
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
$deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
Cookie::set(
'alc_enc',
$this->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
null,
true
);
Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
} else {
Cookie::set('alc_enc', null);
Cookie::set('alc_device', null);
Cookie::force_expiry('alc_enc');
Cookie::force_expiry('alc_device');
}
}
/**
* Called after a member is logged in via session/cookie/etc
*/
public function afterMemberLoggedIn()
{
// Clear the incorrect log-in count
$this->registerSuccessfulLogin();
$this->LockedOutUntil = null;
$this->LockedOutUntil = null;
$this->regenerateTempID();
@ -539,91 +494,37 @@ class Member extends DataObject implements TemplateGlobalProvider
}
/**
* Log the user in if the "remember login" cookie is set
*
* The <i>remember login token</i> will be changed on every successful
* auto-login.
* Logs this member out.
*/
public static function autoLogin()
public function logOut()
{
// Don't bother trying this multiple times
if (!class_exists(SapphireTest::class, false) || !SapphireTest::is_running_test()) {
self::$_already_tried_to_auto_log_in = true;
$this->extend('beforeMemberLoggedOut');
Session::clear("loggedInAs");
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, null, 0);
}
if (!Security::config()->autologin_enabled
|| strpos(Cookie::get('alc_enc'), ':') === false
|| Session::get("loggedInAs")
|| !Security::database_is_ready()
) {
return;
}
Session::destroy();
if (strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
$this->extend('memberLoggedOut');
if (!$uid || !$token) {
return;
}
// Clears any potential previous hashes for this member
RememberLoginHash::clear($this, Cookie::get('alc_device'));
$deviceID = 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');
/** @var Member $member */
$member = Member::get()->byID($uid);
// 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');
/** @var RememberLoginHash $rememberLoginHash */
$rememberLoginHash = null;
$this->write();
// check if autologin token matches
if ($member) {
$hash = $member->encryptWithUserSettings($token);
$rememberLoginHash = RememberLoginHash::get()
->filter(array(
'MemberID' => $member->ID,
'DeviceID' => $deviceID,
'Hash' => $hash
))->first();
if (!$rememberLoginHash) {
$member = null;
} else {
// Check for expired token
$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
$now = DBDatetime::now();
$now = new DateTime($now->Rfc2822());
if ($now > $expiryDate) {
$member = null;
}
}
}
if ($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
if ($rememberLoginHash) {
$rememberLoginHash->renew();
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
Cookie::set(
'alc_enc',
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
false,
true
);
}
$member->write();
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
}
// Audit logging hook
$this->extend('memberLoggedOut');
}
/**
@ -820,20 +721,9 @@ class Member extends DataObject implements TemplateGlobalProvider
*/
public static function currentUser()
{
$id = Member::currentUserID();
if ($id) {
return DataObject::get_by_id(Member::class, $id);
}
return Security::getCurrentUser();
}
/**
* Allow override of the current user ID
*
* @var int|null Set to null to fallback to session, or an explicit ID
*/
protected static $overrideID = null;
/**
* Temporarily act as the specified user, limited to a $callback, but
* without logging in as that user.
@ -851,13 +741,18 @@ class Member extends DataObject implements TemplateGlobalProvider
*/
public static function actAs($member, $callback)
{
$id = ($member instanceof Member ? $member->ID : $member) ?: 0;
$previousID = static::$overrideID;
static::$overrideID = $id;
$previousUser = Security::getCurrentUser();
// Transform ID to member
if (is_numeric($member)) {
$member = DataObject::get_by_id(Member::class, $member);
}
Security::setCurrentUser($member);
try {
return $callback();
} finally {
static::$overrideID = $previousID;
Security::setCurrentUser($previousUser);
}
}
@ -868,22 +763,13 @@ class Member extends DataObject implements TemplateGlobalProvider
*/
public static function currentUserID()
{
if (isset(static::$overrideID)) {
return static::$overrideID;
if ($member = Security::getCurrentUser()) {
return $member->ID;
} else {
return 0;
}
$id = Session::get("loggedInAs");
if (!$id && !self::$_already_tried_to_auto_log_in) {
self::autoLogin();
$id = Session::get("loggedInAs");
}
return is_numeric($id) ? $id : 0;
}
private static $_already_tried_to_auto_log_in = false;
/*
* Generate a random password, with randomiser to kick in if there's no words file on the
* filesystem.

View File

@ -3,12 +3,13 @@
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FormRequestHandler;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\IdentityStore;
class ChangePasswordHandler extends FormRequestHandler
{
@ -18,7 +19,7 @@ class ChangePasswordHandler extends FormRequestHandler
* @param array $data The user submitted data
* @return HTTPResponse
*/
public function doChangePassword(array $data)
public function doChangePassword(array $data, $form)
{
$member = Member::currentUser();
// The user was logged in, check the current password
@ -80,7 +81,8 @@ class ChangePasswordHandler extends FormRequestHandler
$member->write();
if ($member->canLogIn()->isValid()) {
$member->logIn();
Injector::inst()->get(IdentityStore::class)
->logIn($member, false, $form->getRequestHandler()->getRequest());
}
// TODO Add confirmation message to login redirect

View File

@ -0,0 +1,223 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Security\AuthenticationHandler as AuthenticationHandlerInterface;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Control\Cookie;
/**
* Authenticate a member pased on a session cookie
*/
class CookieAuthenticationHandler implements AuthenticationHandlerInterface, IdentityStore
{
private $deviceCookieName;
private $tokenCookieName;
private $cascadeLogInTo;
/**
* Get the name of the cookie used to track this device
*
* @return string
*/
public function getDeviceCookieName()
{
return $this->deviceCookieName;
}
/**
* Set the name of the cookie used to track this device
*
* @param string $cookieName
* @return null
*/
public function setDeviceCookieName($deviceCookieName)
{
$this->deviceCookieName = $deviceCookieName;
}
/**
* Get the name of the cookie used to store an login token
*
* @return string
*/
public function getTokenCookieName()
{
return $this->tokenCookieName;
}
/**
* Set the name of the cookie used to store an login token
*
* @param string $cookieName
* @return null
*/
public function setTokenCookieName($tokenCookieName)
{
$this->tokenCookieName = $tokenCookieName;
}
/**
* Once a member is found by authenticateRequest() pass it to this identity store
*
* @return IdentityStore
*/
public function getCascadeLogInTo()
{
return $this->cascadeLogInTo;
}
/**
* Set the name of the cookie used to store an login token
*
* @param $cascadeLogInTo
* @return null
*/
public function setCascadeLogInTo(IdentityStore $cascadeLogInTo)
{
$this->cascadeLogInTo = $cascadeLogInTo;
}
/**
* @inherit
*/
public function authenticateRequest(HTTPRequest $request)
{
$uidAndToken = Cookie::get($this->getTokenCookieName());
$deviceID = Cookie::get($this->getDeviceCookieName());
// @todo Consider better placement of database_is_ready test
if (!$deviceID || strpos($uidAndToken, ':') === false || !Security::database_is_ready()) {
return;
}
list($uid, $token) = explode(':', $uidAndToken, 2);
if (!$uid || !$token) {
return;
}
/** @var Member $member */
$member = Member::get()->byID($uid);
/** @var RememberLoginHash $rememberLoginHash */
$rememberLoginHash = null;
// check if autologin token matches
if ($member) {
$hash = $member->encryptWithUserSettings($token);
$rememberLoginHash = RememberLoginHash::get()
->filter(array(
'MemberID' => $member->ID,
'DeviceID' => $deviceID,
'Hash' => $hash
))->first();
if (!$rememberLoginHash) {
$member = null;
} else {
// Check for expired token
$expiryDate = new \DateTime($rememberLoginHash->ExpiryDate);
$now = DBDatetime::now();
$now = new \DateTime($now->Rfc2822());
if ($now > $expiryDate) {
$member = null;
}
}
}
if ($member) {
if ($this->cascadeLogInTo) {
// @todo look at how to block "regular login" triggers from happening here
// @todo deal with the fact that the Session::current_session() isn't correct here :-/
$this->cascadeLogInTo->logIn($member, false, $request);
//\SilverStripe\Dev\Debug::message('here');
}
// @todo Consider whether response should be part of logIn() as well
// Renew the token
if ($rememberLoginHash) {
$rememberLoginHash->renew();
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
Cookie::set(
$this->getTokenCookieName(),
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
false,
true
);
}
return $member;
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
}
/**
* @inherit
*/
public function logIn(Member $member, $persistent, HTTPRequest $request)
{
// @todo couple the cookies to the response object
// Cleans up any potential previous hash for this member on this device
if ($alcDevice = Cookie::get($this->getDeviceCookieName())) {
RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
}
// Set a cookie for persistent log-ins
if ($persistent) {
$rememberLoginHash = RememberLoginHash::generate($member);
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
$deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
Cookie::set(
$this->getTokenCookieName(),
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
null,
true
);
Cookie::set(
$this->getDeviceCookieName(),
$rememberLoginHash->DeviceID,
$deviceExpiryDays,
null,
null,
null,
true
);
// Clear a cookie for non-persistent log-ins
} else {
$this->logOut($request);
}
}
/**
* @inherit
*/
public function logOut(HTTPRequest $request)
{
// @todo couple the cookies to the response object
Cookie::set($this->getTokenCookieName(), null);
Cookie::set($this->getDeviceCookieName(), null);
Cookie::force_expiry($this->getTokenCookieName());
Cookie::force_expiry($this->getDeviceCookieName());
}
}

View File

@ -9,6 +9,8 @@ use SilverStripe\Control\RequestHandler;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Security;
use SilverStripe\Security\Member;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Security\IdentityStore;
/**
* Handle login requests from MemberLoginForm
@ -99,7 +101,7 @@ class LoginHandler extends RequestHandler
// Successful login
if ($member = $this->checkLogin($data, $failureMessage)) {
$this->performLogin($member, $data);
$this->performLogin($member, $data, $form->getRequestHandler()->getRequest());
return $this->redirectAfterSuccessfulLogin();
}
@ -220,9 +222,10 @@ class LoginHandler extends RequestHandler
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($member, $data)
public function performLogin($member, $data, $request)
{
$member->logIn(isset($data['Remember']));
// @todo pass request/response
Injector::inst()->get(IdentityStore::class)->logIn($member, !empty($data['Remember']), $request);
return $member;
}
/**

View File

@ -0,0 +1,112 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Control\Director;
use SilverStripe\Security\AuthenticationHandler as AuthenticationHandlerInterface;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\IdentityStore;
/**
* Authenticate a member pased on a session cookie
*/
class SessionAuthenticationHandler implements AuthenticationHandlerInterface, IdentityStore
{
private $sessionVariable;
/**
* Get the session variable name used to track member ID
*
* @return string
*/
public function getSessionVariable()
{
return $this->sessionVariable;
}
/**
* Set the session variable name used to track member ID
*
* @param string $sessionVariable
* @return null
*/
public function setSessionVariable($sessionVariable)
{
$this->sessionVariable = $sessionVariable;
}
/**
* @inherit
*/
public function authenticateRequest(HTTPRequest $request)
{
// @todo couple the session to a request object
// $session = $request->getSession();
if ($id = Session::get($this->getSessionVariable())) {
// If ID is a bad ID it will be treated as if the user is not logged in, rather than throwing a
// ValidationException
return DataObject::get_by_id(Member::class, $id);
}
return null;
}
/**
* @inherit
*/
public function logIn(Member $member, $persistent, HTTPRequest $request)
{
// @todo couple the session to a request object
// $session = $request->getSession();
$this->regenerateSessionId();
Session::set($this->getSessionVariable(), $member->ID);
// This lets apache rules detect whether the user has logged in
// @todo make this a settign on the authentication handler
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0);
}
}
/**
* Regenerate the session_id.
*/
protected static function regenerateSessionId()
{
if (!Member::config()->session_regenerate_id) {
return;
}
// This can be called via CLI during testing.
if (Director::is_cli()) {
return;
}
$file = '';
$line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function!
if (!headers_sent($file, $line)) {
@session_regenerate_id(true);
}
}
/**
* @inherit
*/
public function logOut(HTTPRequest $request)
{
// @todo couple the session to a request object
// $session = $request->getSession();
Session::clear($this->getSessionVariable());
}
}

View File

@ -428,6 +428,18 @@ class Security extends Controller implements TemplateGlobalProvider
));
}
private static $currentUser;
public static function setCurrentUser($currentUser)
{
self::$currentUser = $currentUser;
}
public static function getCurrentUser()
{
return self::$currentUser;
}
/**
* Get the login forms for all available authentication methods
*

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Security\Tests;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult;
@ -13,6 +14,7 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator\Authenticator;
use SilverStripe\Security\MemberAuthenticator\LoginForm;
use SilverStripe\Security\CMSMemberLoginForm;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
@ -103,7 +105,7 @@ class MemberAuthenticatorTest extends SapphireTest
$this->assertEmpty($tempID);
// If the user logs in then they have a temp id
$member->logIn(true);
Injector::inst()->get(IdentityStore::class)->logIn($member, true, new HTTPRequest('GET', '/'));
$tempID = $member->TempIDHash;
$this->assertNotEmpty($tempID);

View File

@ -15,10 +15,12 @@ use SilverStripe\Security\Security;
use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\Group;
use SilverStripe\Security\Permission;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\PasswordEncryptor_Blowfish;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Member_Validator;
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
use SilverStripe\Control\HTTPRequest;
class MemberTest extends FunctionalTest
{
@ -534,26 +536,25 @@ class MemberTest extends FunctionalTest
$member = $this->objFromFixture(Member::class, 'test');
$member2 = $this->objFromFixture(Member::class, 'staffmember');
$this->session()->inst_set('loggedInAs', null);
/* Not logged in, you can't view, delete or edit the record */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
/* Logged in users can edit their own record */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->logInAs($member);
$this->assertTrue($member->canView());
$this->assertFalse($member->canDelete());
$this->assertTrue($member->canEdit());
/* Other uses cannot view, delete or edit others records */
$this->session()->inst_set('loggedInAs', $member2->ID);
$this->logInAs($member2);
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
$this->session()->inst_set('loggedInAs', null);
$this->addExtensions($extensions);
$this->logOut();
}
public function testAuthorisedMembersCanManipulateOthersRecords()
@ -562,14 +563,19 @@ class MemberTest extends FunctionalTest
$member2 = $this->objFromFixture(Member::class, 'staffmember');
/* Group members with SecurityAdmin permissions can manipulate other records */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->logInAs($member);
$this->assertTrue($member2->canView());
$this->assertTrue($member2->canDelete());
$this->assertTrue($member2->canEdit());
$this->addExtensions($extensions);
$this->logOut();
}
public function testExtendedCan()
{
$extensions = $this->removeExtensions(Object::get_extensions(Member::class));
$member = $this->objFromFixture(Member::class, 'test');
/* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
@ -664,12 +670,12 @@ class MemberTest extends FunctionalTest
'Adding new admin group relation is not allowed for non-admin members'
);
$this->session()->inst_set('loggedInAs', $adminMember->ID);
$this->logInAs($adminMember);
$this->assertTrue(
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
);
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
$this->assertTrue(
$adminMember->onChangeGroups(array($newAdminGroup->ID)),
@ -719,7 +725,7 @@ class MemberTest extends FunctionalTest
);
// Test staff member can be added if they are already admin
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
$this->assertFalse($adminMember->inGroup($newAdminGroup));
$adminMember->Groups()->add($newAdminGroup);
$this->assertTrue(
@ -872,7 +878,8 @@ class MemberTest extends FunctionalTest
{
$m1 = $this->objFromFixture(Member::class, 'grouplessmember');
$m1->login(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
$this->assertEquals($hashes->count(), 1);
$firstHash = $hashes->first();
@ -887,7 +894,8 @@ class MemberTest extends FunctionalTest
*/
$m1 = $this->objFromFixture(Member::class, 'noexpiry');
$m1->logIn(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash);
@ -914,7 +922,7 @@ class MemberTest extends FunctionalTest
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
// A wrong token or a wrong device ID should not let us autologin
$response = $this->get(
@ -922,7 +930,7 @@ class MemberTest extends FunctionalTest
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.str_rot13($token),
'alc_enc' => $m1->ID.':asdfasd'.str_rot13($token),
'alc_device' => $firstHash->DeviceID
)
);
@ -965,7 +973,7 @@ class MemberTest extends FunctionalTest
* @var Member $m1
*/
$m1 = $this->objFromFixture(Member::class, 'noexpiry');
$m1->logIn(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash);
@ -995,7 +1003,7 @@ class MemberTest extends FunctionalTest
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
@ -1015,7 +1023,7 @@ class MemberTest extends FunctionalTest
)
);
$this->assertNotContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
DBDatetime::clear_mock_now();
}
@ -1024,10 +1032,10 @@ class MemberTest extends FunctionalTest
$m1 = $this->objFromFixture(Member::class, 'noexpiry');
// First device
$m1->login(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
Cookie::set('alc_device', null);
// Second device
$m1->login(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
// Hash of first device
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
@ -1068,7 +1076,7 @@ class MemberTest extends FunctionalTest
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
// Accessing the login page from the second device
$response = $this->get(
@ -1100,7 +1108,7 @@ class MemberTest extends FunctionalTest
// Logging out from any device when all login hashes should be removed
RememberLoginHash::config()->update('logout_across_devices', true);
$m1->login(true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true, new HTTPRequest('GET', '/'));
$response = $this->get('Security/logout', $this->session());
$this->assertEquals(
RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),

View File

@ -378,6 +378,8 @@ class SecurityTest extends FunctionalTest
);
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
$this->logOut();
/* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */
$expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword');
$this->assertEquals(302, $expiredResponse->getStatusCode());
@ -415,6 +417,7 @@ class SecurityTest extends FunctionalTest
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
// Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals(
@ -460,6 +463,7 @@ class SecurityTest extends FunctionalTest
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
// Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
@ -532,7 +536,7 @@ class SecurityTest extends FunctionalTest
);
// Log the user out
$this->session()->inst_set('loggedInAs', null);
$this->logOut();
// Login again with wrong password, but less attempts than threshold
for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {