Modular authentication.

This commit is contained in:
flash 2020-05-12 18:30:22 +00:00
parent 8b2b5c40c9
commit 245878f7f8
8 changed files with 181 additions and 88 deletions

View file

@ -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
View 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

View file

@ -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)

View 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
View 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
View 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
View 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
View 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();
}
}