Updated authentication token format.
This commit is contained in:
parent
5d602bd56f
commit
0bb1303413
12 changed files with 337 additions and 64 deletions
53
assets/css/misuzu/impersonate.css
Normal file
53
assets/css/misuzu/impersonate.css
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
.impersonate {
|
||||||
|
--start-colour: var(--accent-colour);
|
||||||
|
--end-colour: var(--background-colour);
|
||||||
|
background-image: repeating-linear-gradient(-45deg, var(--start-colour), var(--start-colour) 10px, var(--end-colour) 10px, var(--end-colour) 20px);
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impersonate-content {
|
||||||
|
max-width: var(--site-max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impersonate-user {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background-color: #222d;
|
||||||
|
}
|
||||||
|
.impersonate-user-link {
|
||||||
|
color: var(--user-colour);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.impersonate-user-link:hover,
|
||||||
|
.impersonate-user-link:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.impersonate-user-avatar {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impersonate-options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.impersonate-options-link {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 29px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5em;
|
||||||
|
background-color: #222d;
|
||||||
|
display: block;
|
||||||
|
color: var(--text-colour);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color .1s;
|
||||||
|
}
|
||||||
|
.impersonate-options-link:focus,
|
||||||
|
.impersonate-options-link:hover {
|
||||||
|
background-color: #555d;
|
||||||
|
}
|
||||||
|
.impersonate-options-link:active {
|
||||||
|
background-color: #333d;
|
||||||
|
}
|
42
misuzu.php
42
misuzu.php
|
@ -161,24 +161,27 @@ Template::set('globals', [
|
||||||
|
|
||||||
Template::addPath(MSZ_TEMPLATES);
|
Template::addPath(MSZ_TEMPLATES);
|
||||||
|
|
||||||
|
AuthToken::setSecretKey($cfg->getValue('auth.secret', IConfig::T_STR, 'meow'));
|
||||||
|
|
||||||
if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
|
if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
|
||||||
$authToken = (new AuthToken)
|
$authToken = new AuthToken;
|
||||||
->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0)
|
$authToken->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0);
|
||||||
->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid') ?? '');
|
$authToken->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid') ?? '');
|
||||||
|
|
||||||
if($authToken->isValid())
|
if($authToken->isValid())
|
||||||
setcookie('msz_auth', $authToken->pack(), strtotime('1 year'), '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
$authToken->applyCookie(strtotime('1 year'));
|
||||||
|
|
||||||
setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
|
AuthToken::nukeCookieLegacy();
|
||||||
setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isset($authToken))
|
if(!isset($authToken))
|
||||||
$authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth') ?? '');
|
$authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth') ?? '');
|
||||||
|
|
||||||
if($authToken->isValid()) {
|
if($authToken->isValid()) {
|
||||||
|
$authToken->setCurrent();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$sessionInfo = $authToken->getSession();
|
$sessionInfo = UserSession::byToken($authToken->getSessionToken());
|
||||||
if($sessionInfo->hasExpired()) {
|
if($sessionInfo->hasExpired()) {
|
||||||
$sessionInfo->delete();
|
$sessionInfo->delete();
|
||||||
} elseif($sessionInfo->getUserId() === $authToken->getUserId()) {
|
} elseif($sessionInfo->getUserId() === $authToken->getUserId()) {
|
||||||
|
@ -189,7 +192,22 @@ if($authToken->isValid()) {
|
||||||
$sessionInfo->bump($_SERVER['REMOTE_ADDR']);
|
$sessionInfo->bump($_SERVER['REMOTE_ADDR']);
|
||||||
|
|
||||||
if($sessionInfo->shouldBumpExpire())
|
if($sessionInfo->shouldBumpExpire())
|
||||||
setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
$authToken->applyCookie($sessionInfo->getExpiresTime());
|
||||||
|
|
||||||
|
// only allow impersonation when super user
|
||||||
|
if($authToken->hasImpersonatedUserId() && $userInfo->isSuper()) {
|
||||||
|
$userInfoReal = $userInfo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userInfo = User::byId($authToken->getImpersonatedUserId());
|
||||||
|
} catch(UserNotFoundException $ex) {
|
||||||
|
$userInfo = $userInfoReal;
|
||||||
|
$authToken->removeImpersonatedUserId();
|
||||||
|
$authToken->applyCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo->setCurrent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(UserNotFoundException $ex) {
|
} catch(UserNotFoundException $ex) {
|
||||||
|
@ -202,10 +220,8 @@ if($authToken->isValid()) {
|
||||||
|
|
||||||
if(UserSession::hasCurrent()) {
|
if(UserSession::hasCurrent()) {
|
||||||
$userInfo->bumpActivity($_SERVER['REMOTE_ADDR']);
|
$userInfo->bumpActivity($_SERVER['REMOTE_ADDR']);
|
||||||
} else {
|
} else
|
||||||
setcookie('msz_auth', '', -9001, '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
AuthToken::nukeCookie();
|
||||||
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CSRF::setGlobalSecretKey($cfg->getValue('csrf.secret', IConfig::T_STR, 'soup'));
|
CSRF::setGlobalSecretKey($cfg->getValue('csrf.secret', IConfig::T_STR, 'soup'));
|
||||||
|
@ -248,6 +264,8 @@ if(parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH) !== '/index.php')
|
||||||
|
|
||||||
if(!empty($userInfo))
|
if(!empty($userInfo))
|
||||||
Template::set('current_user', $userInfo);
|
Template::set('current_user', $userInfo);
|
||||||
|
if(!empty($userInfoReal))
|
||||||
|
Template::set('current_user_real', $userInfoReal);
|
||||||
|
|
||||||
$inManageMode = str_starts_with($_SERVER['REQUEST_URI'], '/manage');
|
$inManageMode = str_starts_with($_SERVER['REQUEST_URI'], '/manage');
|
||||||
$hasManageAccess = User::hasCurrent()
|
$hasManageAccess = User::hasCurrent()
|
||||||
|
|
|
@ -119,7 +119,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$authToken = AuthToken::create($userInfo, $sessionInfo);
|
$authToken = AuthToken::create($userInfo, $sessionInfo);
|
||||||
setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
$authToken->applyCookie($sessionInfo->getExpiresTime());
|
||||||
|
|
||||||
if(!is_local_url($loginRedirect))
|
if(!is_local_url($loginRedirect))
|
||||||
$loginRedirect = url('index');
|
$loginRedirect = url('index');
|
||||||
|
|
|
@ -12,8 +12,7 @@ if(!UserSession::hasCurrent()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(CSRF::validateRequest()) {
|
if(CSRF::validateRequest()) {
|
||||||
setcookie('msz_auth', '', -9001, '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
AuthToken::nukeCookie();
|
||||||
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
|
|
||||||
UserSession::getCurrent()->delete();
|
UserSession::getCurrent()->delete();
|
||||||
UserSession::unsetCurrent();
|
UserSession::unsetCurrent();
|
||||||
User::unsetCurrent();
|
User::unsetCurrent();
|
||||||
|
|
21
public/auth/revert.php
Normal file
21
public/auth/revert.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use Misuzu\Users\User;
|
||||||
|
|
||||||
|
require_once '../../misuzu.php';
|
||||||
|
|
||||||
|
if(!isset($userInfoReal) || !$authToken->hasImpersonatedUserId() || !CSRF::validateRequest()) {
|
||||||
|
url_redirect('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authToken->removeImpersonatedUserId();
|
||||||
|
$authToken->applyCookie();
|
||||||
|
|
||||||
|
$impUserId = User::hasCurrent() ? User::getCurrent()->getId() : 0;
|
||||||
|
|
||||||
|
url_redirect(
|
||||||
|
$impUserId > 0 ? 'manage-user' : 'index',
|
||||||
|
['user' => $impUserId]
|
||||||
|
);
|
|
@ -83,7 +83,7 @@ while(!empty($twofactor)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$authToken = AuthToken::create($userInfo, $sessionInfo);
|
$authToken = AuthToken::create($userInfo, $sessionInfo);
|
||||||
setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
$authToken->applyCookie($sessionInfo->getExpiresTime());
|
||||||
|
|
||||||
if(!is_local_url($redirect)) {
|
if(!is_local_url($redirect)) {
|
||||||
$redirect = url('index');
|
$redirect = url('index');
|
||||||
|
|
|
@ -31,6 +31,19 @@ $canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ
|
||||||
$permissions = manage_perms_list(perms_get_user_raw($userId));
|
$permissions = manage_perms_list(perms_get_user_raw($userId));
|
||||||
|
|
||||||
if(CSRF::validateRequest() && $canEdit) {
|
if(CSRF::validateRequest() && $canEdit) {
|
||||||
|
if(!empty($_POST['impersonate_user'])) {
|
||||||
|
if(!$currentUser->isSuper()) {
|
||||||
|
$notices[] = 'You must be a super user to do this.';
|
||||||
|
} elseif(!is_string($_POST['impersonate_user']) || $_POST['impersonate_user'] !== 'meow') {
|
||||||
|
$notices[] = 'You didn\'t say the magic word.';
|
||||||
|
} else {
|
||||||
|
$authToken->setImpersonatedUserId($userInfo->getId());
|
||||||
|
$authToken->applyCookie();
|
||||||
|
url_redirect('index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!empty($_POST['send_test_email'])) {
|
if(!empty($_POST['send_test_email'])) {
|
||||||
if(!$currentUser->isSuper()) {
|
if(!$currentUser->isSuper()) {
|
||||||
$notices[] = 'You must be a super user to do this.';
|
$notices[] = 'You must be a super user to do this.';
|
||||||
|
|
|
@ -3,66 +3,103 @@ namespace Misuzu;
|
||||||
|
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
use Misuzu\Users\UserSession;
|
use Misuzu\Users\UserSession;
|
||||||
|
use Index\IO\MemoryStream;
|
||||||
use Index\Serialisation\Serialiser;
|
use Index\Serialisation\Serialiser;
|
||||||
|
|
||||||
class AuthToken {
|
class AuthToken {
|
||||||
public const VERSION = 1;
|
private const EPOCH = 1682985600;
|
||||||
public const WIDTH = 37;
|
|
||||||
|
|
||||||
private $userId = -1;
|
private int $timestamp = 0;
|
||||||
private $sessionToken = '';
|
private int $cookieExpires = 0;
|
||||||
|
private array $props = [];
|
||||||
|
|
||||||
private $user = null;
|
private static string $secretKey = '';
|
||||||
private $session = null;
|
|
||||||
|
public static function setSecretKey(string $secretKey): void {
|
||||||
|
self::$secretKey = $secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimestamp(): int {
|
||||||
|
return $this->timestamp;
|
||||||
|
}
|
||||||
|
public function updateTimestamp(): void {
|
||||||
|
$this->timestamp = self::timestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasProperty(string $name): bool {
|
||||||
|
return isset($this->props[$name]);
|
||||||
|
}
|
||||||
|
public function getProperty(string $name): string {
|
||||||
|
return $this->props[$name] ?? '';
|
||||||
|
}
|
||||||
|
public function setProperty(string $name, string $value): void {
|
||||||
|
$this->props[$name] = $value;
|
||||||
|
$this->updateTimestamp();
|
||||||
|
}
|
||||||
|
public function removeProperty(string $name): void {
|
||||||
|
unset($this->props[$name]);
|
||||||
|
$this->updateTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
public function isValid(): bool {
|
public function isValid(): bool {
|
||||||
return $this->getUserId() > 0
|
if($this->getUserId() < 1 || empty($this->getSessionToken()))
|
||||||
&& !empty($this->getSessionToken());
|
return false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUserId(): int {
|
public function getUserId(): int {
|
||||||
return $this->userId < 1 ? -1 : $this->userId;
|
$value = (int)$this->getProperty('u');
|
||||||
|
return $value < 1 ? -1 : $value;
|
||||||
}
|
}
|
||||||
public function setUserId(int $userId): self {
|
public function setUserId(int $userId): self {
|
||||||
$this->user = null;
|
$this->setProperty('u', (string)$userId);
|
||||||
$this->userId = $userId;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
public function getUser(): User {
|
|
||||||
if($this->user === null)
|
|
||||||
$this->user = User::byId($this->getUserId());
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
public function setUser(User $user): self {
|
|
||||||
$this->user = $user;
|
|
||||||
$this->userId = $user->getId();
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSessionToken(): string {
|
public function getSessionToken(): string {
|
||||||
return $this->sessionToken ?? '';
|
if(!$this->hasProperty('t'))
|
||||||
|
return '';
|
||||||
|
return bin2hex($this->getProperty('t'));
|
||||||
}
|
}
|
||||||
public function setSessionToken(string $token): self {
|
public function setSessionToken(string $token): self {
|
||||||
$this->session = null;
|
$this->setProperty('t', hex2bin($token));
|
||||||
$this->sessionToken = $token;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
public function getSession(): UserSession {
|
|
||||||
if($this->session === null)
|
|
||||||
$this->session = UserSession::byToken($this->getSessionToken());
|
|
||||||
return $this->session;
|
|
||||||
}
|
|
||||||
public function setSession(UserSession $session): self {
|
|
||||||
$this->session = $session;
|
|
||||||
$this->sessionToken = $session->getToken();
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasImpersonatedUserId(): bool {
|
||||||
|
return $this->hasProperty('i');
|
||||||
|
}
|
||||||
|
public function getImpersonatedUserId(): int {
|
||||||
|
$value = (int)$this->getProperty('i');
|
||||||
|
return $value < 1 ? -1 : $value;
|
||||||
|
}
|
||||||
|
public function setImpersonatedUserId(int $userId): void {
|
||||||
|
$this->setProperty('i', (string)$userId);
|
||||||
|
}
|
||||||
|
public function removeImpersonatedUserId(): void {
|
||||||
|
$this->removeProperty('i');
|
||||||
|
}
|
||||||
|
|
||||||
public function pack(bool $base64 = true): string {
|
public function pack(bool $base64 = true): string {
|
||||||
$packed = pack('CNH*', self::VERSION, $this->getUserId(), $this->getSessionToken());
|
$data = '';
|
||||||
|
|
||||||
|
foreach($this->props as $name => $value) {
|
||||||
|
// very smart solution for this issue, you definitely won't be confused by this later
|
||||||
|
// down the line when a variable suddenly despawns from the token
|
||||||
|
$nameLength = strlen($name);
|
||||||
|
$valueLength = strlen($value);
|
||||||
|
if($nameLength > 255 || $valueLength > 255)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$data .= chr($nameLength) . $name . chr($valueLength) . $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = pack('CN', 2, $this->getTimestamp());
|
||||||
|
$data = $prefix . hash_hmac('sha3-256', $prefix . $data, self::$secretKey, true) . $data;
|
||||||
if($base64)
|
if($base64)
|
||||||
$packed = Serialiser::uriBase64()->serialise($packed);
|
$data = Serialiser::uriBase64()->serialise($data);
|
||||||
return $packed;
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function unpack(string $data, bool $base64 = true): self {
|
public static function unpack(string $data, bool $base64 = true): self {
|
||||||
|
@ -72,20 +109,105 @@ class AuthToken {
|
||||||
return $obj;
|
return $obj;
|
||||||
if($base64)
|
if($base64)
|
||||||
$data = Serialiser::uriBase64()->deserialise($data);
|
$data = Serialiser::uriBase64()->deserialise($data);
|
||||||
|
if(empty($data))
|
||||||
|
return $obj;
|
||||||
|
|
||||||
$data = str_pad($data, self::WIDTH, "\x00");
|
$version = ord($data[0]);
|
||||||
$data = unpack('Cversion/Nuser/H*token', $data);
|
$data = substr($data, 1);
|
||||||
|
|
||||||
if($data['version'] >= 1)
|
if($version === 1) {
|
||||||
$obj->setUserId($data['user'])
|
$data = str_pad($data, 36, "\x00");
|
||||||
->setSessionToken($data['token']);
|
$data = unpack('Nuser/H*token', $data);
|
||||||
|
|
||||||
|
$obj->props['u'] = (string)$data['user'];
|
||||||
|
$obj->props['t'] = hex2bin($data['token']);
|
||||||
|
$obj->updateTimestamp();
|
||||||
|
} elseif($version === 2) {
|
||||||
|
$timestamp = substr($data, 0, 4);
|
||||||
|
$userHash = substr($data, 4, 32);
|
||||||
|
$data = substr($data, 36);
|
||||||
|
$realHash = hash_hmac('sha3-256', chr($version) . $timestamp . $data, self::$secretKey, true);
|
||||||
|
|
||||||
|
if(!hash_equals($realHash, $userHash))
|
||||||
|
return $obj;
|
||||||
|
|
||||||
|
extract(unpack('Ntimestamp', $timestamp));
|
||||||
|
$obj->timestamp = $timestamp;
|
||||||
|
|
||||||
|
$stream = MemoryStream::fromString($data);
|
||||||
|
$stream->seek(0);
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
$length = $stream->readChar();
|
||||||
|
if($length === null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
$length = ord($length);
|
||||||
|
if($length < 1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
$name = $stream->read($length);
|
||||||
|
$value = null;
|
||||||
|
$length = $stream->readChar();
|
||||||
|
if($length !== null) {
|
||||||
|
$length = ord($length);
|
||||||
|
if($length > 0)
|
||||||
|
$value = $stream->read($length);
|
||||||
|
}
|
||||||
|
|
||||||
|
$obj->props[$name] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $obj;
|
return $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function timestamp(): int {
|
||||||
|
return time() - self::EPOCH;
|
||||||
|
}
|
||||||
|
|
||||||
public static function create(User $user, UserSession $session): self {
|
public static function create(User $user, UserSession $session): self {
|
||||||
return (new AuthToken)
|
$token = new AuthToken;
|
||||||
->setUser($user)
|
$token->setUserId($user->getId());
|
||||||
->setSession($session);
|
$token->setSessionToken($session->getToken());
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyCookie(int $expires = 0): void {
|
||||||
|
if($expires > 0)
|
||||||
|
$this->cookieExpires = $expires;
|
||||||
|
else
|
||||||
|
$expires = $this->cookieExpires;
|
||||||
|
|
||||||
|
setcookie('msz_auth', $this->pack(), $expires, '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function nukeCookie(): void {
|
||||||
|
setcookie('msz_auth', '', -9001, '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
||||||
|
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function nukeCookieLegacy(): void {
|
||||||
|
setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
|
||||||
|
setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// please never use the below functions beyond the scope of the sharpchat auth stuff
|
||||||
|
// a better mechanism for keeping a global instance of this available
|
||||||
|
// that isn't a $GLOBAL variable or static instance needs to be established, for User and UserSession as well
|
||||||
|
|
||||||
|
private static $localToken = null;
|
||||||
|
|
||||||
|
public function setCurrent(): void {
|
||||||
|
self::$localToken = $this;
|
||||||
|
}
|
||||||
|
public static function unsetCurrent(): void {
|
||||||
|
self::$localToken = null;
|
||||||
|
}
|
||||||
|
public static function getCurrent(): ?self {
|
||||||
|
return self::$localToken;
|
||||||
|
}
|
||||||
|
public static function hasCurrent(): bool {
|
||||||
|
return self::$localToken !== null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,16 +126,22 @@ final class SharpChatRoutes {
|
||||||
if($request->getMethod() === 'OPTIONS')
|
if($request->getMethod() === 'OPTIONS')
|
||||||
return 204;
|
return 204;
|
||||||
|
|
||||||
if(!UserSession::hasCurrent())
|
if(!UserSession::hasCurrent() || !AuthToken::hasCurrent())
|
||||||
return ['ok' => false];
|
return ['ok' => false];
|
||||||
|
|
||||||
|
$token = AuthToken::getCurrent();
|
||||||
$session = UserSession::getCurrent();
|
$session = UserSession::getCurrent();
|
||||||
|
if($session->getToken() !== $token->getSessionToken())
|
||||||
|
return ['ok' => false];
|
||||||
|
|
||||||
$user = $session->getUser();
|
$user = $session->getUser();
|
||||||
$token = AuthToken::create($user, $session);
|
$userId = $token->hasImpersonatedUserId() && $user->isSuper()
|
||||||
|
? $token->getImpersonatedUserId()
|
||||||
|
: $user->getId();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'usr' => $user->getId(),
|
'usr' => $userId,
|
||||||
'tkn' => $token->pack(),
|
'tkn' => $token->pack(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -281,6 +287,15 @@ final class SharpChatRoutes {
|
||||||
$sessionInfo->bump($ipAddress);
|
$sessionInfo->bump($ipAddress);
|
||||||
|
|
||||||
$userInfo = $sessionInfo->getUser();
|
$userInfo = $sessionInfo->getUser();
|
||||||
|
if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuper()) {
|
||||||
|
$userInfoReal = $userInfo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userInfo = User::byId($authTokenInfo->getImpersonatedUserId());
|
||||||
|
} catch(UserNotFoundException $ex) {
|
||||||
|
$userInfo = $userInfoReal;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ['success' => false, 'reason' => 'unsupported'];
|
return ['success' => false, 'reason' => 'unsupported'];
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ define('MSZ_URLS', [
|
||||||
'auth-logout' => ['/auth/logout.php', ['csrf' => '{csrf}']],
|
'auth-logout' => ['/auth/logout.php', ['csrf' => '{csrf}']],
|
||||||
'auth-resolve-user' => ['/auth/login.php', ['resolve' => '1', 'name' => '<username>']],
|
'auth-resolve-user' => ['/auth/login.php', ['resolve' => '1', 'name' => '<username>']],
|
||||||
'auth-two-factor' => ['/auth/twofactor.php', ['token' => '<token>']],
|
'auth-two-factor' => ['/auth/twofactor.php', ['token' => '<token>']],
|
||||||
|
'auth-revert' => ['/auth/revert.php', ['csrf' => '{csrf}']],
|
||||||
|
|
||||||
'changelog-index' => ['/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>']],
|
'changelog-index' => ['/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>']],
|
||||||
'changelog-feed-rss' => ['/changelog.rss'],
|
'changelog-feed-rss' => ['/changelog.rss'],
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
{% from 'macros.twig' import avatar %}
|
{% from 'macros.twig' import avatar %}
|
||||||
{% from '_layout/input.twig' import input_checkbox_raw %}
|
{% from '_layout/input.twig' import input_checkbox_raw %}
|
||||||
|
|
||||||
|
{% if current_user_real is defined %}
|
||||||
|
<div class="impersonate">
|
||||||
|
<div class="impersonate-content">
|
||||||
|
<div class="impersonate-user" style="--user-colour: {{ current_user_real.colour }}">
|
||||||
|
You are <a href="{{ url('user-profile', {'user': current_user_real.id}) }}" class="impersonate-user-link">
|
||||||
|
<div class="avatar impersonate-user-avatar">{{ avatar(current_user_real.id, 20, current_user_real.username) }}</div>
|
||||||
|
{{ current_user_real.username }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="impersonate-options">
|
||||||
|
<a href="{{ url('auth-revert') }}" class="impersonate-options-link" title="Revert"><i class="fas fa-backward"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<nav class="header">
|
<nav class="header">
|
||||||
<div class="header__background"></div>
|
<div class="header__background"></div>
|
||||||
|
|
||||||
|
|
|
@ -145,6 +145,21 @@
|
||||||
<button class="input__button manage__user__button">Send</button>
|
<button class="input__button manage__user__button">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
|
||||||
|
{{ container_title('Impersonate ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
|
||||||
|
|
||||||
|
<div class="container__content">
|
||||||
|
<p>Uses a special token to completely impersonate this user for testing.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ input_csrf() }}
|
||||||
|
{{ input_hidden('impersonate_user', 'meow') }}
|
||||||
|
|
||||||
|
<div class="manage__user__buttons">
|
||||||
|
<button class="input__button manage__user__button">Impersonate</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
|
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
|
||||||
|
|
Loading…
Reference in a new issue