Modular authentication.
This commit is contained in:
parent
8b2b5c40c9
commit
245878f7f8
8 changed files with 181 additions and 88 deletions
|
@ -3,7 +3,7 @@ namespace EEPROM;
|
||||||
|
|
||||||
function mszDieIfNotAuth(): void {
|
function mszDieIfNotAuth(): void {
|
||||||
if(!User::hasActive()) {
|
if(!User::hasActive()) {
|
||||||
$mszUserId = checkMszAuth(strval(filter_input(INPUT_COOKIE, 'msz_auth')));
|
$mszUserId = (new Auth\MisuzuAuth)->verifyToken(strval(filter_input(INPUT_COOKIE, 'msz_auth')));
|
||||||
if($mszUserId > 0)
|
if($mszUserId > 0)
|
||||||
User::byId($mszUserId)->setActive();
|
User::byId($mszUserId)->setActive();
|
||||||
}
|
}
|
||||||
|
|
24
config.example.ini
Normal file
24
config.example.ini
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[PDO]
|
||||||
|
dsn = https://www.php.net/manual/en/ref.pdo-mysql.connection.php
|
||||||
|
username = mariadb username
|
||||||
|
password = mariadb password
|
||||||
|
|
||||||
|
[Auth]
|
||||||
|
; Must be implementations of \EEPROM\Auth\AuthInterface
|
||||||
|
clients[] = \EEPROM\Auth\MisuzuAuth
|
||||||
|
clients[] = \EEPROM\Auth\SockChatAuth
|
||||||
|
|
||||||
|
[Misuzu]
|
||||||
|
config = /path/to/misuzu/config.ini
|
||||||
|
|
||||||
|
[Nabucco]
|
||||||
|
secret = secret key
|
||||||
|
|
||||||
|
[Uploads]
|
||||||
|
short_domain = i.eeprom.domain
|
||||||
|
|
||||||
|
[CORS]
|
||||||
|
; List of allowed remote domains
|
||||||
|
origins[] = flashii.net
|
||||||
|
origins[] = chat.flashii.net
|
||||||
|
origins[] = sockchat.flashii.net
|
|
@ -22,13 +22,12 @@ function eepromOriginAllowed(string $origin): bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
function eepromByteSymbol(int $bytes, bool $decimal = true, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
|
function eepromByteSymbol(int $bytes, bool $decimal = true, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
|
||||||
if($bytes < 1) {
|
if($bytes < 1)
|
||||||
return '0 B';
|
return '0 B';
|
||||||
}
|
|
||||||
|
|
||||||
$divider = $decimal ? 1000 : 1024;
|
$divider = $decimal ? 1000 : 1024;
|
||||||
$exp = floor(log($bytes) / log($divider));
|
$exp = floor(log($bytes) / log($divider));
|
||||||
$bytes = $bytes / pow($divider, floor($exp));
|
$bytes = $bytes / pow($divider, $exp);
|
||||||
$symbol = $symbols[$exp];
|
$symbol = $symbols[$exp];
|
||||||
|
|
||||||
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
||||||
|
@ -62,95 +61,19 @@ if($reqMethod === 'OPTIONS') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectMszDatabase(): \PDO {
|
|
||||||
global $mszPdo;
|
|
||||||
|
|
||||||
if($mszPdo)
|
|
||||||
return $mszPdo;
|
|
||||||
|
|
||||||
$configPath = Config::get('Misuzu', 'config', '');
|
|
||||||
|
|
||||||
if(!is_file($configPath))
|
|
||||||
throw new \Exception('Cannot find Misuzu configuration.');
|
|
||||||
|
|
||||||
$config = parse_ini_file($configPath, true)['Database'];
|
|
||||||
$dsn = ($config['driver'] ?? 'mysql') . ':';
|
|
||||||
|
|
||||||
foreach($config as $key => $value) {
|
|
||||||
if($key === 'driver' || $key === 'username' || $key === 'password')
|
|
||||||
continue;
|
|
||||||
if($key === 'database')
|
|
||||||
$key = 'dbname';
|
|
||||||
|
|
||||||
$dsn .= $key . '=' . $value . ';';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$mszPdo = new \PDO($dsn, $config['username'], $config['password'], DB::FLAGS);
|
|
||||||
} catch(\PDOException $ex) {
|
|
||||||
throw new \Exception('Unable to connect to Misuzu database.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $mszPdo;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkSockChatAuth(string $token): int {
|
|
||||||
if(strpos($token, '_') === false)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
$mszPdo = connectMszDatabase();
|
|
||||||
$tokenParts = explode('_', $token, 2);
|
|
||||||
$userId = intval($tokenParts[0] ?? 0);
|
|
||||||
$chatToken = strval($tokenParts[1] ?? '');
|
|
||||||
|
|
||||||
$getUserId = $mszPdo->prepare('
|
|
||||||
SELECT `user_id`
|
|
||||||
FROM `msz_user_chat_tokens`
|
|
||||||
WHERE `user_id` = :user
|
|
||||||
AND `token_string` = :token
|
|
||||||
AND `token_created` > NOW() - INTERVAL 1 WEEK
|
|
||||||
');
|
|
||||||
$getUserId->bindValue('user', $userId);
|
|
||||||
$getUserId->bindValue('token', $chatToken);
|
|
||||||
$getUserId->execute();
|
|
||||||
|
|
||||||
return (int)$getUserId->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkMszAuth(string $token): int {
|
|
||||||
$packed = Base64::decode($token, true);
|
|
||||||
$packed = str_pad($packed, 37, "\x00");
|
|
||||||
$unpacked = unpack('Cversion/Nuser/H64token', $packed);
|
|
||||||
|
|
||||||
if($unpacked['version'] !== 1)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
$getUserId = connectMszDatabase()->prepare('
|
|
||||||
SELECT `user_id`
|
|
||||||
FROM `msz_sessions`
|
|
||||||
WHERE `user_id` = :user
|
|
||||||
AND `session_key` = :token
|
|
||||||
AND `session_expires` > NOW()
|
|
||||||
');
|
|
||||||
$getUserId->bindValue('user', $unpacked['user']);
|
|
||||||
$getUserId->bindValue('token', $unpacked['token']);
|
|
||||||
$getUserId->execute();
|
|
||||||
|
|
||||||
return (int)$getUserId->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isset($isShortDomain) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
if(!isset($isShortDomain) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||||
$authParts = explode(' ', $_SERVER['HTTP_AUTHORIZATION'], 2);
|
$authParts = explode(' ', $_SERVER['HTTP_AUTHORIZATION'], 2);
|
||||||
$authMethod = strval($authParts[0] ?? '');
|
$authMethod = strval($authParts[0] ?? '');
|
||||||
$authToken = strval($authParts[1] ?? '');
|
$authToken = strval($authParts[1] ?? '');
|
||||||
|
|
||||||
switch($authMethod) {
|
$authClients = Config::get('Auth', 'clients', []);
|
||||||
case 'SockChat':
|
|
||||||
$authUserId = checkSockChatAuth($authToken);
|
foreach($authClients as $client) {
|
||||||
break;
|
$client = new $client;
|
||||||
case 'Misuzu':
|
if($client->getName() !== $authMethod)
|
||||||
$authUserId = checkMszAuth($authToken);
|
continue;
|
||||||
break;
|
$authUserId = $client->verifyToken($authToken);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($authUserId) && $authUserId > 0)
|
if(isset($authUserId) && $authUserId > 0)
|
||||||
|
|
7
src/Auth/AuthInterface.php
Normal file
7
src/Auth/AuthInterface.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
namespace EEPROM\Auth;
|
||||||
|
|
||||||
|
interface AuthInterface {
|
||||||
|
public function getName(): string;
|
||||||
|
public function verifyToken(string $token): int;
|
||||||
|
}
|
66
src/Auth/MisuzuAuth.php
Normal file
66
src/Auth/MisuzuAuth.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
namespace EEPROM\Auth;
|
||||||
|
|
||||||
|
use EEPROM\Base64;
|
||||||
|
use EEPROM\Config;
|
||||||
|
use EEPROM\DB;
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class MisuzuAuth implements AuthInterface {
|
||||||
|
private static $database = null;
|
||||||
|
|
||||||
|
public function getDatabase(): PDO {
|
||||||
|
if(static::$database !== null)
|
||||||
|
return static::$database;
|
||||||
|
|
||||||
|
$configPath = Config::get('Misuzu', 'config', '');
|
||||||
|
|
||||||
|
if(!is_file($configPath))
|
||||||
|
throw new \Exception('Cannot find Misuzu configuration.');
|
||||||
|
|
||||||
|
$config = parse_ini_file($configPath, true)['Database'];
|
||||||
|
$dsn = ($config['driver'] ?? 'mysql') . ':';
|
||||||
|
|
||||||
|
foreach($config as $key => $value) {
|
||||||
|
if($key === 'driver' || $key === 'username' || $key === 'password')
|
||||||
|
continue;
|
||||||
|
if($key === 'database')
|
||||||
|
$key = 'dbname';
|
||||||
|
|
||||||
|
$dsn .= $key . '=' . $value . ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
static::$database = new PDO($dsn, $config['username'], $config['password'], DB::FLAGS);
|
||||||
|
} catch(PDOException $ex) {
|
||||||
|
throw new \Exception('Unable to connect to Misuzu database.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::$database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string { return 'Misuzu'; }
|
||||||
|
|
||||||
|
public function verifyToken(string $token): int {
|
||||||
|
$packed = Base64::decode($token, true);
|
||||||
|
$packed = str_pad($packed, 37, "\x00");
|
||||||
|
$unpacked = unpack('Cversion/Nuser/H64token', $packed);
|
||||||
|
|
||||||
|
if($unpacked['version'] !== 1)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
$getUserId = $this->getDatabase()->prepare('
|
||||||
|
SELECT `user_id`
|
||||||
|
FROM `msz_sessions`
|
||||||
|
WHERE `user_id` = :user
|
||||||
|
AND `session_key` = :token
|
||||||
|
AND `session_expires` > NOW()
|
||||||
|
');
|
||||||
|
$getUserId->bindValue('user', $unpacked['user']);
|
||||||
|
$getUserId->bindValue('token', $unpacked['token']);
|
||||||
|
$getUserId->execute();
|
||||||
|
|
||||||
|
return (int)$getUserId->fetchColumn();
|
||||||
|
}
|
||||||
|
}
|
35
src/Auth/NabuccoAuth.php
Normal file
35
src/Auth/NabuccoAuth.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
namespace EEPROM\Auth;
|
||||||
|
|
||||||
|
use EEPROM\Base64;
|
||||||
|
use EEPROM\Config;
|
||||||
|
|
||||||
|
class NabuccoAuth implements AuthInterface {
|
||||||
|
private $secretKey = '';
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->secretKey = Config::get('Nabucco', 'secret', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string { return 'Nabucco'; }
|
||||||
|
|
||||||
|
public function hashToken(string $token): string {
|
||||||
|
return hash_hmac('md5', $token, $this->secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyToken(string $token): int {
|
||||||
|
$length = strlen($token);
|
||||||
|
if($length < 32 || $length > 100)
|
||||||
|
return -1;
|
||||||
|
$userHash = substr($token, 0, 32);
|
||||||
|
$packed = Base64::decode(substr($token, 32), true);
|
||||||
|
$realHash = $this->hashToken($packed);
|
||||||
|
if(!hash_equals($realHash, $userHash))
|
||||||
|
return -1;
|
||||||
|
$unpacked = unpack('NuserId/Ntime/CipWidth/a16ipAddr', $packed);
|
||||||
|
if(empty($unpacked['userId']) || empty($unpacked['time'])
|
||||||
|
|| $unpacked['time'] < strtotime('-1 month'))
|
||||||
|
return -1;
|
||||||
|
return intval($unpacked['userId'] ?? -1);
|
||||||
|
}
|
||||||
|
}
|
10
src/Auth/OAuthAuth.php
Normal file
10
src/Auth/OAuthAuth.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
namespace EEPROM\Auth;
|
||||||
|
|
||||||
|
class OAuthAuth implements AuthInterface {
|
||||||
|
public function getName(): string { return 'OAuth'; }
|
||||||
|
|
||||||
|
public function verifyToken(string $token): int {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
28
src/Auth/SockChatAuth.php
Normal file
28
src/Auth/SockChatAuth.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
namespace EEPROM\Auth;
|
||||||
|
|
||||||
|
class SockChatAuth extends MisuzuAuth {
|
||||||
|
public function getName(): string { return 'SockChat'; }
|
||||||
|
|
||||||
|
public function verifyToken(string $token): int {
|
||||||
|
if(strpos($token, '_') === false)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
$tokenParts = explode('_', $token, 2);
|
||||||
|
$userId = intval($tokenParts[0] ?? 0);
|
||||||
|
$chatToken = strval($tokenParts[1] ?? '');
|
||||||
|
|
||||||
|
$getUserId = $this->getDatabase()->prepare('
|
||||||
|
SELECT `user_id`
|
||||||
|
FROM `msz_user_chat_tokens`
|
||||||
|
WHERE `user_id` = :user
|
||||||
|
AND `token_string` = :token
|
||||||
|
AND `token_created` > NOW() - INTERVAL 1 WEEK
|
||||||
|
');
|
||||||
|
$getUserId->bindValue('user', $userId);
|
||||||
|
$getUserId->bindValue('token', $chatToken);
|
||||||
|
$getUserId->execute();
|
||||||
|
|
||||||
|
return (int)$getUserId->fetchColumn();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue