2016-02-04 23:21:06 +01:00
|
|
|
<?php
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2016-06-23 01:37:22 +02:00
|
|
|
namespace SilverStripe\Security;
|
|
|
|
|
|
|
|
use DateInterval;
|
2017-06-09 05:07:35 +02:00
|
|
|
use DateTime;
|
|
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
2016-06-23 01:37:22 +02:00
|
|
|
|
2016-02-04 23:21:06 +01:00
|
|
|
/**
|
|
|
|
* Persists a token associated with a device for users who opted for the "Remember Me"
|
|
|
|
* feature when logging in.
|
|
|
|
* By default, logging out will discard all existing tokens for this user
|
|
|
|
* The device ID is a temporary ID associated with the device when the user logged in
|
2016-03-08 21:50:18 +01:00
|
|
|
* and chose to get the login state remembered on this device. When logging out, the ID
|
2016-02-04 23:21:06 +01:00
|
|
|
* is discarded as well.
|
|
|
|
*
|
|
|
|
* @property string $DeviceID
|
2017-06-09 05:07:35 +02:00
|
|
|
* @property string $ExpiryDate
|
|
|
|
* @property string $Hash
|
2016-08-19 00:51:35 +02:00
|
|
|
* @method Member Member()
|
2016-02-04 23:21:06 +01:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class RememberLoginHash extends DataObject
|
|
|
|
{
|
2017-01-25 04:35:13 +01:00
|
|
|
private static $singular_name = 'Login Hash';
|
|
|
|
|
|
|
|
private static $plural_name = 'Login Hashes';
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $db = [
|
2016-11-29 00:31:16 +01:00
|
|
|
'DeviceID' => 'Varchar(40)',
|
|
|
|
'Hash' => 'Varchar(160)',
|
|
|
|
'ExpiryDate' => 'Datetime'
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $has_one = [
|
2017-01-25 04:35:13 +01:00
|
|
|
'Member' => Member::class,
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $indexes = [
|
2016-11-29 00:31:16 +01:00
|
|
|
'DeviceID' => true,
|
|
|
|
'Hash' => true
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
private static $table_name = "RememberLoginHash";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if logging out on one device also clears existing login tokens
|
|
|
|
* on all other devices owned by the member.
|
|
|
|
* If set to false, there is no way for users to revoke a login, unless additional
|
|
|
|
* code (custom or with a module) is provided by the developer
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $logout_across_devices = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of days the token will be valid for
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var int
|
|
|
|
*/
|
2021-04-06 03:22:10 +02:00
|
|
|
private static $token_expiry_days = 30;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of days the device ID will be valid for
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private static $device_expiry_days = 365;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, user can only use auto login on one device. A user can still login from multiple
|
|
|
|
* devices, but previous tokens from other devices will become invalid.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $force_single_token = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The token used for the hash
|
|
|
|
*/
|
|
|
|
private $token = null;
|
|
|
|
|
2021-04-19 03:13:35 +02:00
|
|
|
/**
|
|
|
|
* Used to override the config value of logout_across_devices
|
|
|
|
* Tri-state where null denotes an unset override value
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
* @var bool|null
|
|
|
|
*/
|
|
|
|
protected static $logoutAcrossDevices = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function getLogoutAcrossDevices(): bool
|
|
|
|
{
|
|
|
|
if (is_bool(static::$logoutAcrossDevices)) {
|
|
|
|
return static::$logoutAcrossDevices;
|
|
|
|
}
|
|
|
|
return static::config()->get('logout_across_devices');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Override the config value of logout_across_devices for the duration of the request
|
|
|
|
* Useful if an authenticator is causing the wrong number of devices to loose their tokens
|
|
|
|
* A value of false will prevent all devices from having their token removed when a single device logs out
|
|
|
|
* A value of true will remove all devices tokens when a single device logs out
|
|
|
|
* Use this public API instead of modifying the config value directly
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
* @param bool $value
|
|
|
|
*/
|
|
|
|
public static function setLogoutAcrossDevices(bool $value): void
|
|
|
|
{
|
|
|
|
static::$logoutAcrossDevices = $value;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
public function getToken()
|
|
|
|
{
|
|
|
|
return $this->token;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setToken($token)
|
|
|
|
{
|
|
|
|
$this->token = $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Randomly generates a new ID used for the device
|
|
|
|
* @return string A device ID
|
|
|
|
*/
|
|
|
|
protected function getNewDeviceID()
|
|
|
|
{
|
|
|
|
$generator = new RandomGenerator();
|
|
|
|
return $generator->randomToken('sha1');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new random token and hashes it using the
|
|
|
|
* member information
|
|
|
|
* @param Member $member The logged in user
|
|
|
|
* @return string The hash to be stored in the database
|
|
|
|
*/
|
|
|
|
public function getNewHash(Member $member)
|
|
|
|
{
|
|
|
|
$generator = new RandomGenerator();
|
|
|
|
$this->setToken($generator->randomToken('sha1'));
|
|
|
|
return $member->encryptWithUserSettings($this->token);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a new login hash associated with a device
|
|
|
|
* The device is assigned a globally unique device ID
|
|
|
|
* The returned login hash stores the hashed token in the
|
|
|
|
* database, for this device and this member
|
|
|
|
* @param Member $member The logged in user
|
|
|
|
* @return RememberLoginHash The generated login hash
|
|
|
|
*/
|
|
|
|
public static function generate(Member $member)
|
|
|
|
{
|
|
|
|
if (!$member->exists()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (static::config()->force_single_token) {
|
|
|
|
RememberLoginHash::get()->filter('MemberID', $member->ID)->removeAll();
|
|
|
|
}
|
|
|
|
/** @var RememberLoginHash $rememberLoginHash */
|
|
|
|
$rememberLoginHash = RememberLoginHash::create();
|
|
|
|
do {
|
|
|
|
$deviceID = $rememberLoginHash->getNewDeviceID();
|
|
|
|
} while (RememberLoginHash::get()->filter('DeviceID', $deviceID)->count());
|
|
|
|
|
|
|
|
$rememberLoginHash->DeviceID = $deviceID;
|
|
|
|
$rememberLoginHash->Hash = $rememberLoginHash->getNewHash($member);
|
|
|
|
$rememberLoginHash->MemberID = $member->ID;
|
|
|
|
$now = DBDatetime::now();
|
|
|
|
$expiryDate = new DateTime($now->Rfc2822());
|
|
|
|
$tokenExpiryDays = static::config()->token_expiry_days;
|
2018-01-16 19:39:30 +01:00
|
|
|
$expiryDate->add(new DateInterval('P' . $tokenExpiryDays . 'D'));
|
2016-11-29 00:31:16 +01:00
|
|
|
$rememberLoginHash->ExpiryDate = $expiryDate->format('Y-m-d H:i:s');
|
|
|
|
$rememberLoginHash->extend('onAfterGenerateToken');
|
|
|
|
$rememberLoginHash->write();
|
|
|
|
return $rememberLoginHash;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a new hash for this member but keeps the device ID intact
|
|
|
|
*
|
|
|
|
* @return RememberLoginHash
|
|
|
|
*/
|
|
|
|
public function renew()
|
|
|
|
{
|
|
|
|
$hash = $this->getNewHash($this->Member());
|
|
|
|
$this->Hash = $hash;
|
|
|
|
$this->extend('onAfterRenewToken');
|
|
|
|
$this->write();
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2021-03-31 00:31:52 +02:00
|
|
|
* @param string|null $alcDevice Null when logging out of non-persi-tien session
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public static function clear(Member $member, $alcDevice = null)
|
|
|
|
{
|
|
|
|
if (!$member->exists()) {
|
2021-03-31 00:31:52 +02:00
|
|
|
// If we don't have a valid user, we can't clear any "Remember me" tokens
|
2016-11-29 00:31:16 +01:00
|
|
|
return;
|
|
|
|
}
|
2021-03-31 00:31:52 +02:00
|
|
|
|
2021-04-19 03:13:35 +02:00
|
|
|
if (static::getLogoutAcrossDevices()) {
|
2021-03-31 00:31:52 +02:00
|
|
|
self::get()->filter(['MemberID' => $member->ID])->removeAll();
|
|
|
|
} elseif ($alcDevice) {
|
|
|
|
self::get()->filter([
|
|
|
|
'MemberID' => $member->ID,
|
|
|
|
'DeviceID' => $alcDevice
|
|
|
|
])->removeAll();
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
2016-03-08 21:50:18 +01:00
|
|
|
}
|