silverstripe-framework/src/Security/RememberLoginHash.php

230 lines
6.8 KiB
PHP

<?php
namespace SilverStripe\Security;
use DateInterval;
use DateTime;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
/**
* 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
* and chose to get the login state remembered on this device. When logging out, the ID
* is discarded as well.
*
* @property string $DeviceID
* @property string $ExpiryDate
* @property string $Hash
* @method Member Member()
*/
class RememberLoginHash extends DataObject
{
private static $singular_name = 'Login Hash';
private static $plural_name = 'Login Hashes';
private static $db = [
'DeviceID' => 'Varchar(40)',
'Hash' => 'Varchar(160)',
'ExpiryDate' => 'Datetime'
];
private static $has_one = [
'Member' => Member::class,
];
private static $indexes = [
'DeviceID' => true,
'Hash' => true
];
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
*/
private static $token_expiry_days = 30;
/**
* 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;
/**
* 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;
}
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;
$expiryDate->add(new DateInterval('P' . $tokenExpiryDays . 'D'));
$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
* @param string|null $alcDevice Null when logging out of non-persi-tien session
*/
public static function clear(Member $member, $alcDevice = null)
{
if (!$member->exists()) {
// If we don't have a valid user, we can't clear any "Remember me" tokens
return;
}
if (static::getLogoutAcrossDevices()) {
self::get()->filter(['MemberID' => $member->ID])->removeAll();
} elseif ($alcDevice) {
self::get()->filter([
'MemberID' => $member->ID,
'DeviceID' => $alcDevice
])->removeAll();
}
}
}