diff --git a/src/Auth/Auth.php b/src/Auth/Auth.php index e912359..600944b 100644 --- a/src/Auth/Auth.php +++ b/src/Auth/Auth.php @@ -1,7 +1,82 @@ stmts = new StatementCache($conn); + } + + + public function createLoginSession( + UserInfo $userInfo, + string $remoteAddr, + string $countryCode, + int $factors + ): string { + $loginId = XString::random(48); + + $stmt = $this->stmts->getStatement('create login', function() { + return 'INSERT INTO ' . self::LOGINS_TABLE + . ' (auth_login_id, user_id, auth_login_ip, auth_login_country, auth_login_factors_required)' + . ' VALUES (?, ?, INET6_ATON(?), ?, ?)'; + }); + + $stmt->addParameter(1, $loginId); + $stmt->addParameter(2, $userInfo->getId()); + $stmt->addParameter(3, $remoteAddr); + $stmt->addParameter(4, $countryCode); + $stmt->addParameter(5, $factors); + $stmt->execute(); + + return $loginId; + } + + public function destroyLoginSession(AuthLoginInfo $loginInfo): void { + $stmt = $this->stmts->getStatement('destroy login', fn() => ('DELETE FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?')); + $stmt->addParameter(1, $loginInfo->getId()); + $stmt->execute(); + } + + private function fetchLoginSingle(IDbResult $result, string $exceptionText): UserInfo { + if(!$result->next()) + throw new AuthLoginNotFoundException($exceptionText); + + return new UserInfo($result); + } + + public function getLoginSessionById(string $loginId): ?AuthLoginInfo { + $stmt = $this->stmts->getStatement('get login by id', fn() => ('SELECT ' . implode(',', self::LOGINS_FIELDS) . ' FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?')); + $stmt->addParameter(1, $loginId); + $stmt->execute(); + $result = $stmt->getResult(); + + if(!$result->next()) + return null; + + return new AuthLoginInfo($result); + } + + public function incrementLoginSessionDone(AuthLoginInfo $login): void { + $stmt = $this->stmts->getStatement('increment login done', fn() => ('UPDATE ' . self::LOGINS_TABLE . ' SET auth_login_factors_done = auth_login_factors_done + 1 WHERE auth_login_id = ?')); + $stmt->addParameter(1, $login->getId()); + $stmt->execute(); + } +} diff --git a/src/Auth/Db/DbAuthLogin.php b/src/Auth/AuthLoginInfo.php similarity index 95% rename from src/Auth/Db/DbAuthLogin.php rename to src/Auth/AuthLoginInfo.php index c250349..6d91547 100644 --- a/src/Auth/Db/DbAuthLogin.php +++ b/src/Auth/AuthLoginInfo.php @@ -1,12 +1,11 @@ context->renderTemplate('auth/login', [ - 'userName' => $userName, - 'errorId' => $errorId, - 'errorText' => $errorText, + 'user_name' => $userName, + 'error_name' => $errorId, + 'error_text' => $errorText, ]); } @@ -133,7 +132,7 @@ class AuthRoutes { try { $userInfo = $this->users->getUserInfoByName($userName); - } catch(UserNotFoundException $ex) { + } catch(RuntimeException $ex) { $response->redirect('/login?error=user_not_found&username=' . rawurlencode($userName)); return; } @@ -183,10 +182,10 @@ class AuthRoutes { } return $this->context->renderTemplate('auth/login-tfa', [ - 'userInfo' => $userInfo, - 'authMethods' => $authMethods, - 'authMethodNames' => $authMethodNames, - 'loginSession' => $this->loginSession, + 'user_info' => $userInfo, + 'auth_methods' => $authMethods, + 'auth_method_names' => $authMethodNames, + 'login_session' => $this->loginSession, ]); } @@ -218,8 +217,8 @@ class AuthRoutes { ][$errorId] ?? ''; return $this->context->renderTemplate('auth/login-totp', [ - 'userInfo' => $userInfo, - 'loginSession' => $this->loginSession, + 'user_info' => $userInfo, + 'login_session' => $this->loginSession, ]); } diff --git a/src/Auth/Db/DbAuth.php b/src/Auth/Db/DbAuth.php deleted file mode 100644 index 3fb6c67..0000000 --- a/src/Auth/Db/DbAuth.php +++ /dev/null @@ -1,86 +0,0 @@ -stmts = new StatementCache($conn); - } - - - public function createLoginSession( - IUserInfo $userInfo, - string $remoteAddr, - string $countryCode, - int $factors - ): string { - $loginId = XString::random(48); - - $stmt = $this->stmts->getStatement('create login', function() { - return 'INSERT INTO ' . self::LOGINS_TABLE - . ' (auth_login_id, user_id, auth_login_ip, auth_login_country, auth_login_factors_required)' - . ' VALUES (?, ?, INET6_ATON(?), ?, ?)'; - }); - - $stmt->addParameter(1, $loginId, DbType::STRING); - $stmt->addParameter(2, $userInfo->getId(), DbType::STRING); - $stmt->addParameter(3, $remoteAddr, DbType::STRING); - $stmt->addParameter(4, $countryCode, DbType::STRING); - $stmt->addParameter(5, $factors, DbType::INTEGER); - $stmt->execute(); - - return $loginId; - } - - public function destroyLoginSession(IAuthLogin $loginInfo): void { - $stmt = $this->stmts->getStatement('destroy login', fn() => ('DELETE FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?')); - $stmt->addParameter(1, $loginInfo->getId(), DbType::STRING); - $stmt->execute(); - } - - private function fetchLoginSingle(IDbResult $result, string $exceptionText): IUserInfo { - if(!$result->next()) - throw new AuthLoginNotFoundException($exceptionText); - - return new DbUserInfo($result); - } - - public function getLoginSessionById(string $loginId): ?IAuthLogin { - $stmt = $this->stmts->getStatement('get login by id', fn() => ('SELECT ' . implode(',', self::LOGINS_FIELDS) . ' FROM ' . self::LOGINS_TABLE . ' WHERE auth_login_id = ?')); - $stmt->addParameter(1, $loginId, DbType::STRING); - $stmt->execute(); - $result = $stmt->getResult(); - - if(!$result->next()) - return null; - - return new DbAuthLogin($result); - } - - public function incrementLoginSessionDone(IAuthLogin $login): void { - $stmt = $this->stmts->getStatement('increment login done', fn() => ('UPDATE ' . self::LOGINS_TABLE . ' SET auth_login_factors_done = auth_login_factors_done + 1 WHERE auth_login_id = ?')); - $stmt->addParameter(1, $login->getId(), DbType::STRING); - $stmt->execute(); - } -} diff --git a/src/Auth/IAuthLogin.php b/src/Auth/IAuthLogin.php deleted file mode 100644 index 8a39e9d..0000000 --- a/src/Auth/IAuthLogin.php +++ /dev/null @@ -1,19 +0,0 @@ -config = $config; $this->dbConn = $dbConn; - $this->users = new DbUsers($dbConn); + $this->users = new Users($dbConn); $this->siteInfo = new SiteInfo($config->scopeTo('site')); } - public function getUsers(): IUsers { + public function getUsers(): Users { return $this->users; } @@ -78,7 +76,7 @@ class HanyuuContext { } public function setUpAuth(): void { - $this->auth = new DbAuth($this->dbConn); + $this->auth = new Auth($this->dbConn); } public function getAuth(): Auth { diff --git a/src/OTP/IOTPGenerator.php b/src/OTP/IOTPGenerator.php deleted file mode 100644 index af07b90..0000000 --- a/src/OTP/IOTPGenerator.php +++ /dev/null @@ -1,6 +0,0 @@ -secretKey)) - throw new InvalidArgumentException('$secretKey may not be empty'); - if($this->digits < 1) - throw new InvalidArgumentException('$digits must be a positive integer'); - if($this->interval < 1) - throw new InvalidArgumentException('$interval must be a positive integer'); - if(!in_array($this->hashAlgo, hash_hmac_algos(), true)) - throw new InvalidArgumentException('$hashAlgo must be a hashing algorithm suitable for hmac'); - } - - public function getDigits(): int { - return $this->digits; - } - - public function getInterval(): int { - return $this->interval; - } - - public function getHashAlgo(): string { - return $this->hashAlgo; - } - - public function getTimeCode(int $offset = 0, int $timeStamp = -1): int { - if($timeStamp < 0) - $timeStamp = time(); - - // use -1 and 1 to get the previous and next token for user convenience - if($offset !== 0) - $timeStamp += $this->interval * $offset; - - return (int)(($timeStamp * 1000) / ($this->interval * 1000)); - } - - public function generate(int $offset = 0, int $timeStamp = -1): string { - $timeCode = pack('J', $this->getTimeCode($offset, $timeStamp)); - $secretKey = Serialiser::base32()->deserialise($this->secretKey); - $hash = hash_hmac($this->hashAlgo, $timeCode, $secretKey, true); - - $offset = ord($hash[strlen($hash) - 1]) & 0x0F; - - $bin = (ord($hash[$offset]) & 0x7F) << 24; - $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16; - $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8; - $bin |= ord($hash[$offset + 3]) & 0xFF; - - $otp = (string)($bin % pow(10, $this->digits)); - - return str_pad($otp, $this->digits, '0', STR_PAD_LEFT); - } - - public static function generateKey(int $bytes = 16): string { - if($length < 1) - throw new InvalidArgumentException('$bytes must be a positive integer'); - - return Serialiser::base32()->serialise(random_bytes($bytes)); - } -} diff --git a/src/TOTPGenerator.php b/src/TOTPGenerator.php new file mode 100644 index 0000000..992c9a5 --- /dev/null +++ b/src/TOTPGenerator.php @@ -0,0 +1,52 @@ +secretKey), true); + $offset = ord($hash[strlen($hash) - 1]) & 0x0F; + + $bin = 0; + $bin |= (ord($hash[$offset]) & 0x7F) << 24; + $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16; + $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8; + $bin |= (ord($hash[$offset + 3]) & 0xFF); + $otp = $bin % pow(10, self::DIGITS); + + return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT); + } + + public function generateRange(int $range = 1, ?int $timecode = null): array { + if($range < 1) + throw new InvalidArgumentException('$range must be greater than 0.'); + + $timecode ??= self::timecode(); + $tokens = [$this->generate($timecode)]; + + for($i = 1; $i <= $range; ++$i) + $tokens[] = $this->generate($timecode - $i); + for($i = 1; $i <= $range; ++$i) + $tokens[] = $this->generate($timecode + $i); + + return $tokens; + } +} diff --git a/src/Users/IUserAuthInfo.php b/src/Users/IUserAuthInfo.php deleted file mode 100644 index db3eb2b..0000000 --- a/src/Users/IUserAuthInfo.php +++ /dev/null @@ -1,10 +0,0 @@ -secretKey); } - public function generateValidCodes(int $offset = 0, int $timeStamp = -1): array { - $generator = $this->createGenerator(); - - return [ - $generator->generate(-1 + $offset, $timeStamp), - $generator->generate($offset, $timeStamp), - $generator->generate(1 + $offset, $timeStamp), - ]; + public function generateValidCodes(int $range = 1, ?int $timecode = null): array { + return $this->createGenerator()->generateRange($range, $timecode); } } diff --git a/src/Users/Db/DbUsers.php b/src/Users/Users.php similarity index 70% rename from src/Users/Db/DbUsers.php rename to src/Users/Users.php index 06d2ff0..466220b 100644 --- a/src/Users/Db/DbUsers.php +++ b/src/Users/Users.php @@ -1,21 +1,18 @@ stmts = new StatementCache($conn); } - private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): IUserInfo { + private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): UserInfo { if(!$result->next()) - throw new UserNotFoundException($exceptionText); + throw new RuntimeException($exceptionText); - return new DbUserInfo($result); + return new UserInfo($result); } - public function getUserInfoById(string $userId): IUserInfo { + public function getUserInfoById(string $userId): UserInfo { $stmt = $this->stmts->getStatement('get info by id', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userId, DbType::STRING); + $stmt->addParameter(1, $userId); $stmt->execute(); return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userId found'); } - public function getUserInfoByName(string $userName): IUserInfo { + public function getUserInfoByName(string $userName): UserInfo { $stmt = $this->stmts->getStatement('get info by name', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_name = ?')); - $stmt->addParameter(1, $userName, DbType::STRING); + $stmt->addParameter(1, $userName); $stmt->execute(); return $this->fetchUserInfoSingle($stmt->getResult(), 'no user with $userName found'); } - public function countUserTFAMethods(IUserInfo $userInfo): int { + public function countUserTFAMethods(UserInfo $userInfo): int { $stmt = $this->stmts->getStatement('count user tfa methods', fn() => ('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } - public function getUserTFAMethods(IUserInfo $userInfo): array { + public function getUserTFAMethods(UserInfo $userInfo): array { $stmt = $this->stmts->getStatement('get user tfa methods', fn() => ('SELECT ' . implode(',', self::TFA_FIELDS) . ' FROM ' . self::TFA_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); $array = []; while($result->next()) - $array[] = new DbUserAuthInfo($result); + $array[] = new UserAuthInfo($result); return $array; } - public function checkUserTFAMethod(IUserInfo $userInfo, IAuthMethod $method): bool { + public function checkUserTFAMethod(UserInfo $userInfo, IAuthMethod $method): bool { $stmt = $this->stmts->getStatement('check user auth method', fn() => ('SELECT COUNT(*) FROM ' . self::TFA_TABLE . ' WHERE user_id = ? AND user_tfa_type = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); - $stmt->addParameter(2, $method->getName(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); + $stmt->addParameter(2, $method->getName()); $stmt->execute(); $result = $stmt->getResult(); @@ -109,57 +106,57 @@ class DbUsers implements IUsers { } - public function getUserPasswordInfo(IUserInfo $userInfo): IUserPasswordInfo { + public function getUserPasswordInfo(UserInfo $userInfo): UserPasswordInfo { $stmt = $this->stmts->getStatement('get pwd', fn() => ('SELECT ' . implode(',', self::PASSWORDS_FIELDS) . ' FROM ' . self::PASSWORDS_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('no password info for $userInfo found'); - return new DbUserPasswordInfo($result); + return new UserPasswordInfo($result); } - public function getUserTOTPInfo(IUserInfo $userInfo): IUserTOTPInfo { + public function getUserTOTPInfo(UserInfo $userInfo): UserTOTPInfo { $stmt = $this->stmts->getStatement('get totp', fn() => ('SELECT ' . implode(',', self::TOTP_FIELDS) . ' FROM ' . self::TOTP_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('no totp info for $userInfo found'); - return new DbUserTOTPInfo($result); + return new UserTOTPInfo($result); } - public function getUserEMailInfos(IUserInfo $userInfo): array { + public function getUserEMailInfos(UserInfo $userInfo): array { $stmt = $this->stmts->getStatement('get emails', fn() => ('SELECT ' . implode(',', self::EMAILS_FIELDS) . ' FROM ' . self::EMAILS_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); $array = []; while($result->next()) - $array[] = new DbUserEMailInfo($result); + $array[] = new UserEMailInfo($result); return $array; } - public function getUserBackupInfos(IUserInfo $userInfo): array { + public function getUserBackupInfos(UserInfo $userInfo): array { $stmt = $this->stmts->getStatement('get backups', fn() => ('SELECT ' . implode(',', self::BACKUP_FIELDS) . ' FROM ' . self::BACKUP_TABLE . ' WHERE user_id = ?')); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); + $stmt->addParameter(1, $userInfo->getId()); $stmt->execute(); $result = $stmt->getResult(); $array = []; while($result->next()) - $array[] = new DbUserBackupInfo($result); + $array[] = new UserBackupInfo($result); return $array; }