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 {
|
||||
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)
|
||||
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 {
|
||||
if($bytes < 1) {
|
||||
if($bytes < 1)
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$divider = $decimal ? 1000 : 1024;
|
||||
$exp = floor(log($bytes) / log($divider));
|
||||
$bytes = $bytes / pow($divider, floor($exp));
|
||||
$bytes = $bytes / pow($divider, $exp);
|
||||
$symbol = $symbols[$exp];
|
||||
|
||||
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
||||
|
@ -62,95 +61,19 @@ if($reqMethod === 'OPTIONS') {
|
|||
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'])) {
|
||||
$authParts = explode(' ', $_SERVER['HTTP_AUTHORIZATION'], 2);
|
||||
$authMethod = strval($authParts[0] ?? '');
|
||||
$authToken = strval($authParts[1] ?? '');
|
||||
|
||||
switch($authMethod) {
|
||||
case 'SockChat':
|
||||
$authUserId = checkSockChatAuth($authToken);
|
||||
break;
|
||||
case 'Misuzu':
|
||||
$authUserId = checkMszAuth($authToken);
|
||||
break;
|
||||
$authClients = Config::get('Auth', 'clients', []);
|
||||
|
||||
foreach($authClients as $client) {
|
||||
$client = new $client;
|
||||
if($client->getName() !== $authMethod)
|
||||
continue;
|
||||
$authUserId = $client->verifyToken($authToken);
|
||||
break;
|
||||
}
|
||||
|
||||
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