A new editor needs a username and a password of at least 12 characters. A persistent login needs an unexpired key and an existing account. Removing an account revokes its persistent logins, and its sessions end.
235 lines
12 KiB
PHP
235 lines
12 KiB
PHP
<?php
|
|
namespace Xxx\Tests\Unit;
|
|
|
|
use Xxx\Tests\Support\FakeDb;
|
|
use Xxx\Tests\Support\PhpProcess;
|
|
use Xxx\Tests\Support\SiteData;
|
|
use Xxx\Tests\Support\TestCase;
|
|
|
|
// CSRF tokens, editor logins and persistent logins in session.lib.php
|
|
class SessionTest extends TestCase {
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
ini_set('session.save_path', $this->tmp);
|
|
ini_set('session.use_cookies', '0');
|
|
ini_set('session.cache_limiter', '');
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
if (session_status() === PHP_SESSION_ACTIVE)
|
|
session_destroy();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testCsrfTokenIsRandomAndKept(): void {
|
|
$token = csrf_token();
|
|
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token);
|
|
$this->assertSame($token, csrf_token());
|
|
$this->assertSame($token, $_SESSION['csrf_token']);
|
|
$_SESSION = array();
|
|
$this->assertNotSame($token, csrf_token());
|
|
}
|
|
|
|
public function testCsrfTokenReplacesABadSessionValue(): void {
|
|
$_SESSION['csrf_token'] = array('not a string');
|
|
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', csrf_token());
|
|
}
|
|
|
|
public function testCsrfField(): void {
|
|
$this->assertSame('<input type="hidden" name="csrf_token" value="' . csrf_token() . '" />', csrf_field());
|
|
}
|
|
|
|
public function testCsrfValidAcceptsOnlyTheSessionToken(): void {
|
|
$token = csrf_token();
|
|
$this->assertTrue(csrf_valid($token));
|
|
$this->assertFalse(csrf_valid(strtoupper($token)));
|
|
$this->assertFalse(csrf_valid(''));
|
|
$this->assertFalse(csrf_valid($token . 'x'));
|
|
$this->assertFalse(csrf_valid(array($token)));
|
|
}
|
|
|
|
public function testCsrfValidReadsThePostedToken(): void {
|
|
$token = csrf_token();
|
|
$this->assertFalse(csrf_valid());
|
|
$this->request(array(), array('csrf_token' => $token));
|
|
$this->assertTrue(csrf_valid());
|
|
$this->request(array('csrf_token' => $token));
|
|
$this->assertFalse(csrf_valid(), 'a token in the query string is not enough');
|
|
}
|
|
|
|
public function testCsrfValidFailsWithoutASessionToken(): void {
|
|
$this->assertFalse(csrf_valid(''));
|
|
$this->assertFalse(csrf_valid('anything'));
|
|
}
|
|
|
|
public function testLoginEditorGivesANewSessionId(): void {
|
|
session_start();
|
|
$before = session_id();
|
|
$_SESSION['csrf_token'] = 'old token';
|
|
$_SESSION['referrer'] = '/about';
|
|
login_editor('ed');
|
|
$this->assertNotSame($before, session_id());
|
|
$this->assertSame('ed', $_SESSION['editor']);
|
|
$this->assertArrayNotHasKey('csrf_token', $_SESSION);
|
|
$this->assertSame('/about', $_SESSION['referrer']);
|
|
$this->assertFileDoesNotExist($this->tmp . '/sess_' . $before);
|
|
}
|
|
|
|
public function testPersistentCookieParts(): void {
|
|
$this->assertSame(array('', ''), persistent_cookie());
|
|
$_COOKIE['PersistentAuth'] = 'ed|key|with|bars';
|
|
$this->assertSame(array('ed', 'key|with|bars'), persistent_cookie());
|
|
$_COOKIE['PersistentAuth'] = 'nobar';
|
|
$this->assertSame(array('nobar', ''), persistent_cookie());
|
|
$_COOKIE['PersistentAuth'] = array('ed', 'key');
|
|
$this->assertSame(array('', ''), persistent_cookie());
|
|
}
|
|
|
|
public function testPersistentKeyHash(): void {
|
|
$this->assertSame(hash('sha256', 'key'), persistent_key_hash('key'));
|
|
}
|
|
|
|
public function testCreatePersistentLoginStoresOnlyAHash(): void {
|
|
$this->assertTrue(create_persistent_login('ed'));
|
|
$rows = SiteData::$tables['persistent_sessions'];
|
|
$this->assertCount(1, $rows);
|
|
$this->assertSame('ed', $rows[0]['username']);
|
|
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $rows[0]['session']);
|
|
$this->assertNull($GLOBALS['db'], 'the connection is closed again');
|
|
}
|
|
|
|
public function testCheckPersistentLoginWithoutACookie(): void {
|
|
$this->assertFalse(check_persistent_login());
|
|
$this->assertSame(array(), FakeDb::$log);
|
|
}
|
|
|
|
public function testCheckPersistentLoginWithAnIncompleteCookie(): void {
|
|
foreach (array('', 'ed', 'ed|', '|key') as $cookie) {
|
|
$_COOKIE['PersistentAuth'] = $cookie;
|
|
$this->assertFalse(check_persistent_login(), $cookie);
|
|
}
|
|
$this->assertSame(array(), FakeDb::$log);
|
|
}
|
|
|
|
public function testCheckPersistentLoginWithAWrongKey(): void {
|
|
SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('right'), 'created' => gmdate('Y-m-d H:i:s'));
|
|
$_COOKIE['PersistentAuth'] = 'ed|wrong';
|
|
$this->assertFalse(check_persistent_login());
|
|
$this->assertArrayNotHasKey('editor', $_SESSION);
|
|
$query = FakeDb::matching('/FROM persistent_sessions/')[0];
|
|
$this->assertSame(array('ed', persistent_key_hash('wrong')), $query['params']);
|
|
}
|
|
|
|
public function testCheckPersistentLoginWithAKeyOfAnotherEditor(): void {
|
|
SiteData::$tables['persistent_sessions'][] = array('username' => 'ann', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s'));
|
|
$_COOKIE['PersistentAuth'] = 'ed|key';
|
|
$this->assertFalse(check_persistent_login());
|
|
}
|
|
|
|
public function testCheckPersistentLoginLogsInAndSwapsTheKey(): void {
|
|
SiteData::$tables['editors'][] = array('username' => 'ed', 'password' => '', 'email' => '');
|
|
session_start();
|
|
$before = session_id();
|
|
SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s'));
|
|
$_COOKIE['PersistentAuth'] = 'ed|key';
|
|
$this->assertTrue(check_persistent_login());
|
|
$this->assertSame('ed', $_SESSION['editor']);
|
|
$this->assertNotSame($before, session_id());
|
|
$rows = SiteData::$tables['persistent_sessions'];
|
|
$this->assertCount(1, $rows);
|
|
$this->assertNotSame(persistent_key_hash('key'), $rows[0]['session'], 'the used key is gone');
|
|
}
|
|
|
|
public function testCheckPersistentLoginRefusesAnExpiredKey(): void {
|
|
SiteData::$tables['editors'][] = array('username' => 'ed', 'password' => '', 'email' => '');
|
|
SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('key'),
|
|
'created' => gmdate('Y-m-d H:i:s', strtotime('-1 year -1 day')));
|
|
$_COOKIE['PersistentAuth'] = 'ed|key';
|
|
$this->assertFalse(check_persistent_login());
|
|
$this->assertArrayNotHasKey('editor', $_SESSION ?? array());
|
|
$this->assertSame(array(), SiteData::$tables['persistent_sessions']);
|
|
}
|
|
|
|
public function testCheckPersistentLoginRefusesAKeyWithoutAnAccount(): void {
|
|
SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s'));
|
|
$_COOKIE['PersistentAuth'] = 'ed|key';
|
|
$this->assertFalse(check_persistent_login());
|
|
$this->assertSame(array(), SiteData::$tables['persistent_sessions']);
|
|
}
|
|
|
|
public function testEditorExists(): void {
|
|
SiteData::$tables['editors'][] = array('username' => 'ed', 'password' => '', 'email' => '');
|
|
$this->assertTrue(editor_exists('ed'));
|
|
$this->assertFalse(editor_exists('ann'));
|
|
$this->assertFalse(editor_exists(''));
|
|
$this->assertFalse(editor_exists(array('ed')));
|
|
$this->assertNull($GLOBALS['db'], 'the connection is closed again');
|
|
}
|
|
|
|
public function testRemoveEditorRevokesItsPersistentLogins(): void {
|
|
SiteData::$tables['editors'] = array(array('username' => 'ed', 'password' => '', 'email' => ''),
|
|
array('username' => 'ann', 'password' => '', 'email' => ''));
|
|
SiteData::$tables['persistent_sessions'] = array(
|
|
array('username' => 'ed', 'session' => persistent_key_hash('one'), 'created' => gmdate('Y-m-d H:i:s')),
|
|
array('username' => 'ed', 'session' => persistent_key_hash('two'), 'created' => gmdate('Y-m-d H:i:s')),
|
|
array('username' => 'ann', 'session' => persistent_key_hash('three'), 'created' => gmdate('Y-m-d H:i:s')));
|
|
remove_editor('ed');
|
|
$this->assertSame(array('ann'), array_column(SiteData::$tables['editors'], 'username'));
|
|
$this->assertSame(array('ann'), array_column(SiteData::$tables['persistent_sessions'], 'username'));
|
|
}
|
|
|
|
public function testARemovedEditorIsLoggedOutOverHttps(): void {
|
|
$code = 'Xxx\Tests\Support\SiteData::install(); $_SERVER["HTTPS"] = "on"; session_id("abc123"); session_start(); $_SESSION["editor"] = "ed"; session_write_close();'
|
|
. ' $_COOKIE[session_name()] = "abc123"; include XXX_ROOT . "/session.lib.php";'
|
|
. ' echo json_encode(array($is_admin, $_SESSION["editor"] ?? NULL));';
|
|
$result = PhpProcess::runBare($code, array(), array('session.save_path' => $this->tmp, 'session.use_cookies' => '0', 'session.cache_limiter' => ''));
|
|
$this->assertSame('[false,null]', $result['stdout'], $result['stderr']);
|
|
}
|
|
|
|
public function testDeletePersistentLoginRemovesThisKeyAndOldKeys(): void {
|
|
SiteData::$tables['persistent_sessions'] = array(
|
|
array('username' => 'ed', 'session' => persistent_key_hash('mine'), 'created' => gmdate('Y-m-d H:i:s')),
|
|
array('username' => 'ed', 'session' => persistent_key_hash('other device'), 'created' => gmdate('Y-m-d H:i:s')),
|
|
array('username' => 'ann', 'session' => persistent_key_hash('old'), 'created' => '2000-01-01 00:00:00'),
|
|
array('username' => 'bo', 'session' => 'an unhashed key from before', 'created' => gmdate('Y-m-d H:i:s')),
|
|
);
|
|
$_COOKIE['PersistentAuth'] = 'ed|mine';
|
|
$this->assertTrue(delete_persistent_login());
|
|
$this->assertSame(array(persistent_key_hash('other device')), array_column(SiteData::$tables['persistent_sessions'], 'session'));
|
|
$this->assertCount(1, FakeDb::matching('/INTERVAL 1 YEAR.*CHAR_LENGTH\(session\) <> 64/s'));
|
|
}
|
|
|
|
public function testDeletePersistentLoginWithoutACookieStillReaps(): void {
|
|
delete_persistent_login();
|
|
$this->assertCount(0, FakeDb::matching('/WHERE username = \?/'));
|
|
$this->assertCount(1, FakeDb::matching('/CHAR_LENGTH/'));
|
|
}
|
|
|
|
// The top of session.lib.php runs on every page; it needs HTTPS and a session
|
|
public function testSessionStartsOnlyOverHttps(): void {
|
|
$code = 'Xxx\Tests\Support\SiteData::install(array("editors" => array(array("username" => "ed")))); $_SERVER["HTTPS"] = %s; session_id("abc123"); session_start(); $_SESSION["editor"] = "ed"; session_write_close();'
|
|
. ' $_COOKIE[session_name()] = "abc123"; include XXX_ROOT . "/session.lib.php";'
|
|
. ' echo json_encode(array($is_admin, $username ?? NULL, session_status() === PHP_SESSION_ACTIVE));';
|
|
$ini = array('session.save_path' => $this->tmp, 'session.use_cookies' => '0', 'session.cache_limiter' => '');
|
|
$https = PhpProcess::runBare(sprintf($code, '"on"'), array(), $ini);
|
|
$this->assertSame('[true,"ed",true]', $https['stdout'], $https['stderr']);
|
|
}
|
|
|
|
public function testNoSessionOverPlainHttp(): void {
|
|
$result = PhpProcess::runBare('include XXX_ROOT . "/session.lib.php"; echo json_encode(array($is_admin, session_status() === PHP_SESSION_ACTIVE));',
|
|
array(), array('session.save_path' => $this->tmp));
|
|
$this->assertSame('[false,false]', $result['stdout'], $result['stderr']);
|
|
}
|
|
|
|
public function testPersistentCookieLogsInOverHttps(): void {
|
|
$store = $this->tmp . '/tables.json';
|
|
file_put_contents($store, json_encode(array('editors' => array(array('username' => 'ed')), 'persistent_sessions' => array(
|
|
array('username' => 'ed', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s'))))));
|
|
$code = 'Xxx\Tests\Support\SiteData::install(json_decode(file_get_contents(' . var_export($store, TRUE) . '), TRUE));'
|
|
. ' $_SERVER["HTTPS"] = "on"; $_COOKIE["PersistentAuth"] = "ed|key"; include XXX_ROOT . "/session.lib.php";'
|
|
. ' echo json_encode(array($is_admin, $username ?? NULL));';
|
|
$result = PhpProcess::runBare($code, array(), array('session.save_path' => $this->tmp, 'session.use_cookies' => '0', 'session.cache_limiter' => ''));
|
|
$this->assertSame('[true,"ed"]', $result['stdout'], $result['stderr']);
|
|
}
|
|
}
|