From caeb4af8efc4b2e90144705b422c888c69d21df5 Mon Sep 17 00:00:00 2001 From: flashwave Date: Wed, 18 Oct 2023 10:34:30 +0000 Subject: [PATCH] Backlogged things. --- lib/index | 2 +- public/assets/hanyuu.css | 163 ++++++++++++++++ public/assets/hanyuu.js | 0 src/Auth/Auth.php | 7 + src/Auth/AuthRoutes.php | 281 ++++++++++++++++++++++++++++ src/Auth/Db/DbAuth.php | 86 +++++++++ src/Auth/Db/DbAuthLogin.php | 77 ++++++++ src/Auth/IAuthLogin.php | 19 ++ src/HanyuuContext.php | 33 ++-- src/StatementCache.php | 27 +++ src/Users/Db/DbUserTOTPInfo.php | 10 + src/Users/Db/DbUsers.php | 78 ++++---- src/Users/IUserTOTPInfo.php | 1 + src/Users/UserNotFoundException.php | 6 + templates/auth/login-tfa.php | 38 ++++ templates/auth/login-totp.php | 39 ++++ templates/auth/login.php | 52 +++++ templates/auth/master.php | 13 ++ templates/errors/master.php | 6 +- templates/master.php | 2 + tools/migrate | 0 tools/new-migration | 0 22 files changed, 877 insertions(+), 63 deletions(-) create mode 100644 public/assets/hanyuu.css create mode 100644 public/assets/hanyuu.js create mode 100644 src/Auth/Auth.php create mode 100644 src/Auth/AuthRoutes.php create mode 100644 src/Auth/Db/DbAuth.php create mode 100644 src/Auth/Db/DbAuthLogin.php create mode 100644 src/Auth/IAuthLogin.php create mode 100644 src/StatementCache.php create mode 100644 src/Users/UserNotFoundException.php create mode 100644 templates/auth/login-tfa.php create mode 100644 templates/auth/login-totp.php create mode 100644 templates/auth/login.php create mode 100644 templates/auth/master.php mode change 100755 => 100644 tools/migrate mode change 100755 => 100644 tools/new-migration diff --git a/lib/index b/lib/index index fbe4fe1..bce5ba7 160000 --- a/lib/index +++ b/lib/index @@ -1 +1 @@ -Subproject commit fbe4fe18decd502a0ca15ffe8a7c3b2d847349d5 +Subproject commit bce5ba77a268ecd6338d0e3520e41ff4c40cbeda diff --git a/public/assets/hanyuu.css b/public/assets/hanyuu.css new file mode 100644 index 0000000..e719060 --- /dev/null +++ b/public/assets/hanyuu.css @@ -0,0 +1,163 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + position: relative; +} + +html, body { + width: 100%; + height: 100%; +} + +body { + background-color: #111; + color: #fff; + font: 12px/20px Verdana, Geneva, Arial, Helvetica, sans-serif; +} + +@media (prefers-color-scheme: light) { + body { + background-color: #ddd; + color: #000; + } +} + +[hidden], +.hidden { + display: none !important; +} + +.http-err { + padding: 40px 60px; +} +.http-err-title { + font-size: 3em; + line-height: 1.5em; + font-weight: 700; +} +.http-err-paragraph { + font-size: 1.2em; + line-height: 1.5em; +} + +.auth { + margin: 0 auto; + padding: 10px; + padding-top: 10vh; + box-sizing: content-box; +} + +.auth-header { + font-size: 5em; + line-height: 1.5em; + color: #a77bca; + text-shadow: 0 1px 5px #a77bca; + text-align: center; + padding: 10px 0; +} + +.auth-avatar { + text-align: center; + margin: 10px 0; +} +.auth-avatar-image { + width: 100px; + height: 100px; + overflow: hidden; + border-radius: 5%; + display: inline-block; +} +.auth-avatar img { + vertical-align: middle; + width: 100%; + height: 100%; +} + +.auth-login { + max-width: 400px; + width: 100%; + margin: 0 auto; +} + +.auth-fields { + max-width: 300px; + width: 100%; + margin: 0 auto; + margin-bottom: 10px; +} + +.auth-input { + display: block; + width: 100%; + border-left: 5px solid #aaa; + margin: 10px 0; + padding: 2px 7px; + padding-right: 12px; + transition: border-color .2s; +} +.auth-input:focus-within { + border-color: #a77bca; +} +.auth-input-title { + display: block; + font-size: 1.2em; + line-height: 1.5em; +} +.auth-input-value { + display: block; +} +.auth-input-value input { + display: block; + width: 100%; + border-width: 0; + background-color: inherit; + outline-style: none !important; + font-size: 1.2em; + line-height: 1.5em; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; +} + +.auth-buttons { + max-width: 200px; + width: 100%; + margin: 0 auto; +} + +.auth-button { + display: block; + width: 100%; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 1.5em; + background-color: #bbb; + border-width: 0; + border-radius: 5px; + margin: 5px 0; + padding: 5px; + color: #222; + text-align: center; + text-decoration: none; + cursor: default; + transition: background-color .2s; +} +.auth-button:hover, +.auth-button:focus { + background-color: #ccc; +} +.auth-button:active { + background-color: #aaa; +} + +.auth-button-primary { + font-size: 1.3em; + background-color: #a77bca; + color: #fff; +} +.auth-button-primary:hover, +.auth-button-primary:focus { + background-color: #b28bd1; +} +.auth-button-primary:active { + background-color: #925bbd; +} diff --git a/public/assets/hanyuu.js b/public/assets/hanyuu.js new file mode 100644 index 0000000..e69de29 diff --git a/src/Auth/Auth.php b/src/Auth/Auth.php new file mode 100644 index 0000000..e912359 --- /dev/null +++ b/src/Auth/Auth.php @@ -0,0 +1,7 @@ +context = $ctx; + $this->config = $config; + $this->users = $ctx->getUsers(); + + $this->context->setUpAuth(); + $this->auth = $ctx->getAuth(); + + $router->use('/login', [$this, 'filterLogin']); + + $router->get('/login', [$this, 'getLogin']); + $router->post('/login', [$this, 'postLogin']); + + $router->get('/login/tfa', [$this, 'getLoginTFA']); + $router->get('/login/done', [$this, 'getLoginDone']); + + $router->get('/login/tfa/totp', [$this, 'getLoginTOTP']); + $router->post('/login/tfa/totp', [$this, 'postLoginTOTP']); + + $router->get('/login/tfa/u2f', [$this, 'getLoginU2F']); + $router->post('/login/tfa/u2f', [$this, 'postLoginU2F']); + + $router->get('/login/tfa/backup', [$this, 'getLoginBackup']); + $router->post('/login/tfa/backup', [$this, 'postLoginBackup']); + + $router->get('/register', [$this, 'getRegister']); + + $router->get('/forgot-username', [$this, 'getForgotUserName']); + + $router->get('/forgot-password', [$this, 'getForgotPassword']); + $router->get('/recover-password', [$this, 'getRecoverPassword']); + } + + private function getLoginCookieName(): string { + return $this->config->getValue('login_cookie', IConfig::T_STR, 'hau_login'); + } + + private function destroyLoginSession($response): void { + if($this->loginSession !== null) { + $response->removeCookie($this->getLoginCookieName()); + $this->auth->destroyLoginSession($this->loginSession); + $this->loginSession = null; + } + } + + private function ensureLoginIncomplete($response): bool { + $loginSession = $this->loginSession; + if($loginSession === null) { + $response->redirect('/login'); + return false; + } + + if($loginSession->getFactorsDone() >= $loginSession->getFactorsRequired()) { + $response->redirect('/login/done'); + return false; + } + + return true; + } + + public function filterLogin($response, $request) { + $loginId = (string)$request->getCookie($this->getLoginCookieName()); + $this->loginSession = empty($loginId) ? null : $this->auth->getLoginSessionById($loginId); + + if($this->loginSession !== null) { + if(!$this->loginSession->isValid() + || $this->loginSession->getRemoteAddress()->getCleanAddress() !== $_SERVER['REMOTE_ADDR']) + $this->destroyLoginSession($response); + } + } + + public function getLogin($response, $request) { + $this->destroyLoginSession($response); + + $userName = (string)$request->getParam('username'); + $errorId = (string)$request->getParam('error'); + $errorText = [ + 'invalid_username' => 'Either no username was provided or it was incorrectly formatted.', + 'wrong_password' => 'The provided password was incorrect.', + 'user_not_found' => 'No user with this name exists. NOTE: point to registration somehow', + 'user_deleted' => 'Your account has been marked for deletion. Contact staff to revert this.', + ][$errorId] ?? ''; + + return $this->context->renderTemplate('auth/login', [ + 'userName' => $userName, + 'errorId' => $errorId, + 'errorText' => $errorText, + ]); + } + + public function postLogin($response, $request) { + if(!$request->isFormContent()) + return 400; + $form = $request->getContent(); + + // should be adjusted to respond with json to ajax shit + + $this->destroyLoginSession($response); + + $userName = (string)$form->getParam('username'); + $password = (string)$form->getParam('password'); + + if(empty($userName)) { + $response->redirect('/login?error=invalid_username'); + return; + } + + try { + $userInfo = $this->users->getUserInfoByName($userName); + } catch(UserNotFoundException $ex) { + $response->redirect('/login?error=user_not_found&username=' . rawurlencode($userName)); + return; + } + + if($userInfo->isDeleted()) { + $response->redirect('/login?error=user_deleted'); + return; + } + + $passwordInfo = $this->users->getUserPasswordInfo($userInfo); + if(!$passwordInfo->verifyPassword($password)) { + $response->redirect('/login?error=wrong_password'); + return; + } + + $factorsRequired = $this->users->countUserTFAMethods($userInfo) > 0 ? 2 : 1; + $loginId = $this->auth->createLoginSession($userInfo, $_SERVER['REMOTE_ADDR'], 'A1', $factorsRequired); + $this->loginSession = $this->auth->getLoginSessionById($loginId); + + $this->auth->incrementLoginSessionDone($this->loginSession); + + // check for rehash + + $response->addCookie($this->getLoginCookieName(), $loginId, strtotime('+30 minutes'), '/', '', true, true, true); + + $response->redirect($factorsRequired > 1 ? '/login/tfa' : '/login/done'); + } + + public function getLoginTFA($response, $request) { + if(!$this->ensureLoginIncomplete($response)) + return; + + $errorId = (string)$request->getParam('error'); + $errorText = [ + 'invalid_method' => 'Either no method was provided or it was incorrectly formatted.', + ][$errorId] ?? ''; + + $userInfo = $this->users->getUserInfoById($this->loginSession->getUserId()); + $authMethods = $this->users->getUserTFAMethods($userInfo); + $authMethodNames = []; + foreach($authMethods as $authMethod) + $authMethodNames[] = $authMethod->getType(); + + if(count($authMethods) === 1) { + $response->redirect("/login/tfa/{$authMethods[0]->getType()}"); + return; + } + + return $this->context->renderTemplate('auth/login-tfa', [ + 'userInfo' => $userInfo, + 'authMethods' => $authMethods, + 'authMethodNames' => $authMethodNames, + 'loginSession' => $this->loginSession, + ]); + } + + public function getLoginDone($response, $request) { + if($this->loginSession === null) { + $response->redirect('/login'); + return; + } + + if($this->loginSession->getFactorsDone() < $this->loginSession->getFactorsRequired()) { + $response->redirect('/login/pick'); + return; + } + + $this->destroyLoginSession($response); + + return "you've successfully authenticated but there's no session system yet lol"; + } + + public function getLoginTOTP($response, $request) { + if(!$this->ensureLoginIncomplete($response)) + return; + + $userInfo = $this->users->getUserInfoById($this->loginSession->getUserId()); + + $errorId = (string)$request->getParam('error'); + $errorText = [ + 'invalid_method' => 'Either no method was provided or it was incorrectly formatted.', + ][$errorId] ?? ''; + + return $this->context->renderTemplate('auth/login-totp', [ + 'userInfo' => $userInfo, + 'loginSession' => $this->loginSession, + ]); + } + + public function postLoginTOTP($response, $request) { + if(!$request->isFormContent()) + return 400; + $form = $request->getContent(); + + if(!$this->ensureLoginIncomplete($response)) + return; + + $userInfo = $this->users->getUserInfoById($this->loginSession->getUserId()); + + $totpInfo = $this->users->getUserTOTPInfo($userInfo); + $totpValid = $totpInfo->generateValidCodes(); + $totpCode = (string)$form->getParam('code'); + + if(!in_array($totpCode, $totpValid, true)) { + $response->redirect('/login/tfa/totp?error=wrong_totp'); + return; + } + + $this->auth->incrementLoginSessionDone($this->loginSession); + + $response->redirect('/login/done'); + } + + public function getLoginU2F($response, $request) { + return 503; + } + + public function postLoginU2F($response, $request) { + return 503; + } + + public function getLoginBackup($response, $request) { + return 503; + } + + public function postLoginBackup($response, $request) { + return 503; + } + + public function getRegister($response, $request) { + return 503; + } + + public function getForgotUserName($response, $request) { + return 503; + } + + public function getForgotPassword($response, $request) { + return 503; + } + + public function getRecoverPassword($response, $request) { + return 503; + } +} diff --git a/src/Auth/Db/DbAuth.php b/src/Auth/Db/DbAuth.php new file mode 100644 index 0000000..3fb6c67 --- /dev/null +++ b/src/Auth/Db/DbAuth.php @@ -0,0 +1,86 @@ +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/Db/DbAuthLogin.php b/src/Auth/Db/DbAuthLogin.php new file mode 100644 index 0000000..c250349 --- /dev/null +++ b/src/Auth/Db/DbAuthLogin.php @@ -0,0 +1,77 @@ +id = $result->getString(0); + $this->userId = $result->getString(1); + $this->remoteAddr = new IPAddress($result->getString(2)); + $this->country = $result->getString(3); + $this->factorsRequired = $result->getInteger(4); + $this->factorsDone = $result->getInteger(5); + $this->started = DateTime::fromUnixTimeSeconds($result->getInteger(6)); + $this->valid = DateTime::fromUnixTimeSeconds($result->getInteger(7)); + $this->hasCompleted = !$result->isNull(8); + $this->completed = DateTime::fromUnixTimeSeconds($result->getInteger(8)); + } + + public function getId(): string { + return $this->id; + } + + public function getUserId(): string { + return $this->userId; + } + + public function getRemoteAddress(): IPAddress { + return $this->remoteAddr; + } + + public function getCountryCode(): string { + return $this->country; + } + + public function getFactorsRequired(): int { + return $this->factorsRequired; + } + + public function getFactorsDone(): int { + return $this->factorsDone; + } + + public function getStartedTime(): DateTime { + return $this->started; + } + + public function isValid(): bool { + return DateTime::utcNow()->isLessThan($this->valid); + } + + public function getValidTime(): DateTime { + return $this->valid; + } + + public function hasCompleted(): bool { + return $this->hasCompleted; + } + + public function getCompletedTime(): DateTime { + return $this->completed; + } +} diff --git a/src/Auth/IAuthLogin.php b/src/Auth/IAuthLogin.php new file mode 100644 index 0000000..8a39e9d --- /dev/null +++ b/src/Auth/IAuthLogin.php @@ -0,0 +1,19 @@ +getTemplating()->render(...$args); } + public function setUpAuth(): void { + $this->auth = new DbAuth($this->dbConn); + } + + public function getAuth(): Auth { + return $this->auth; + } + public function getRouter(): IRouter { return $this->router->getRouter(); } @@ -107,25 +119,8 @@ class HanyuuContext { return 503; }); - if(!HAU_DEBUG) - return; + $this->router->get('/coffee', function() { return 418; }); - $this->router->get('/test', function() { - $users = $this->getUsers(); - $userInfo = $users->getUserInfoByName('flAsH'); - - $ffa = $users->getUserFirstFactorAuthInfos($userInfo); - $tfa = $users->getUserSecondFactorAuthInfos($userInfo); - - $pwdInfo = $users->getUserPasswordInfo($userInfo); - - $totpInfo = $users->getUserTOTPInfo($userInfo); - $totpGen = $totpInfo->createGenerator(); - - $emailInfos = $users->getUserEMailInfos($userInfo); - - $backupInfos = $users->getUserBackupInfos($userInfo); - - }); + new AuthRoutes($this->router, $this, $this->config->scopeTo('auth')); } } diff --git a/src/StatementCache.php b/src/StatementCache.php new file mode 100644 index 0000000..14618f6 --- /dev/null +++ b/src/StatementCache.php @@ -0,0 +1,27 @@ +statements)) { + $stmt = $this->statements[$name]; + $stmt->reset(); + } else + $this->statements[$name] = $stmt = $this->conn->prepare($query()); + + return $stmt; + } + + public function clearStatement(string $name): void { + unset($this->statements[$name]); + } +} diff --git a/src/Users/Db/DbUserTOTPInfo.php b/src/Users/Db/DbUserTOTPInfo.php index 759143b..88453c0 100644 --- a/src/Users/Db/DbUserTOTPInfo.php +++ b/src/Users/Db/DbUserTOTPInfo.php @@ -32,4 +32,14 @@ class DbUserTOTPInfo implements IUserTOTPInfo { public function createGenerator(): TOTPGenerator { return new TOTPGenerator($this->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), + ]; + } } diff --git a/src/Users/Db/DbUsers.php b/src/Users/Db/DbUsers.php index 93c7fae..06d2ff0 100644 --- a/src/Users/Db/DbUsers.php +++ b/src/Users/Db/DbUsers.php @@ -6,14 +6,18 @@ use Index\Data\IDbConnection; use Index\Data\IDbStatement; use Index\Data\IDbResult; use Index\Data\DbType; +use Hanyuu\StatementCache; +use Hanyuu\Auth\IAuthMethod; +use Hanyuu\Auth\Auth; use Hanyuu\Users\IUsers; use Hanyuu\Users\IUserInfo; use Hanyuu\Users\IUserPasswordInfo; use Hanyuu\Users\IUserTOTPInfo; +use Hanyuu\Users\UserNotFoundException; class DbUsers implements IUsers { private const USERS_TABLE = 'hau_users'; - private const AUTH_TABLE = 'hau_users_auth'; + private const TFA_TABLE = 'hau_users_tfa'; private const BACKUP_TABLE = 'hau_users_backup'; private const EMAILS_TABLE = 'hau_users_emails'; private const PASSWORDS_TABLE = 'hau_users_passwords'; @@ -23,8 +27,8 @@ class DbUsers implements IUsers { 'user_id', 'user_name', 'user_country', 'user_colour', 'user_super', 'user_time_zone', 'UNIX_TIMESTAMP(user_created)', 'UNIX_TIMESTAMP(user_updated)', 'UNIX_TIMESTAMP(user_deleted)', ]; - private const AUTH_FIELDS = [ - 'user_id', 'user_auth_type', 'UNIX_TIMESTAMP(user_auth_enabled)', + private const TFA_FIELDS = [ + 'user_id', 'user_tfa_type', 'UNIX_TIMESTAMP(user_tfa_enabled)', ]; private const PASSWORDS_FIELDS = [ 'user_id', 'user_password_hash', 'UNIX_TIMESTAMP(user_password_changed)', @@ -39,32 +43,23 @@ class DbUsers implements IUsers { 'user_id', 'user_backup_code', 'UNIX_TIMESTAMP(user_backup_created)', 'UNIX_TIMESTAMP(user_backup_used)', ]; - private const FIRST_FACTOR_AUTH = ['pwd']; - private const SECOND_FACTOR_AUTH = ['totp']; + private StatementCache $stmts; public function __construct( private IDbConnection $conn - ) {} - - private array $statements = []; - - private function getStatement(string $name, callable $query): IDbStatement { - if(array_key_exists($name, $this->statements)) - return $this->statements[$name]; - return $this->statements[$name] = $this->conn->prepare($query()); + ) { + $this->stmts = new StatementCache($conn); } - private function fetchUserInfoSingle(IDbResult $result, string $exceptionText): IUserInfo { if(!$result->next()) - throw new RuntimeException($exceptionText); + throw new UserNotFoundException($exceptionText); return new DbUserInfo($result); } public function getUserInfoById(string $userId): IUserInfo { - $stmt = $this->getStatement('get info by id', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_id = ?')); - $stmt->reset(); + $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->execute(); @@ -72,8 +67,7 @@ class DbUsers implements IUsers { } public function getUserInfoByName(string $userName): IUserInfo { - $stmt = $this->getStatement('get info by name', fn() => ('SELECT ' . implode(',', self::USERS_FIELDS) . ' FROM ' . self::USERS_TABLE . ' WHERE user_name = ?')); - $stmt->reset(); + $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->execute(); @@ -81,7 +75,21 @@ class DbUsers implements IUsers { } - private function fetchAuthInfoMultiple(IDbResult $result): array { + public function countUserTFAMethods(IUserInfo $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->execute(); + $result = $stmt->getResult(); + + return $result->next() ? $result->getInteger(0) : 0; + } + + public function getUserTFAMethods(IUserInfo $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->execute(); + + $result = $stmt->getResult(); $array = []; while($result->next()) @@ -90,28 +98,19 @@ class DbUsers implements IUsers { return $array; } - public function getUserFirstFactorAuthInfos(IUserInfo $userInfo): array { - $stmt = $this->getStatement('get auth first', fn() => ('SELECT ' . implode(',', self::AUTH_FIELDS) . ' FROM ' . self::AUTH_TABLE . ' WHERE user_id = ? AND user_auth_type IN ("' . implode('", "', self::FIRST_FACTOR_AUTH) . '")')); - $stmt->reset(); + public function checkUserTFAMethod(IUserInfo $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->execute(); + $result = $stmt->getResult(); - return $this->fetchAuthInfoMultiple($stmt->getResult()); - } - - public function getUserSecondFactorAuthInfos(IUserInfo $userInfo): array { - $stmt = $this->getStatement('get auth second', fn() => ('SELECT ' . implode(',', self::AUTH_FIELDS) . ' FROM ' . self::AUTH_TABLE . ' WHERE user_id = ? AND user_auth_type IN ("' . implode('", "', self::SECOND_FACTOR_AUTH) . '")')); - $stmt->reset(); - $stmt->addParameter(1, $userInfo->getId(), DbType::STRING); - $stmt->execute(); - - return $this->fetchAuthInfoMultiple($stmt->getResult()); + return $result->next() ? $result->getInteger(0) !== 0 : false; } public function getUserPasswordInfo(IUserInfo $userInfo): IUserPasswordInfo { - $stmt = $this->getStatement('get pwd', fn() => ('SELECT ' . implode(',', self::PASSWORDS_FIELDS) . ' FROM ' . self::PASSWORDS_TABLE . ' WHERE user_id = ?')); - $stmt->reset(); + $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->execute(); $result = $stmt->getResult(); @@ -124,8 +123,7 @@ class DbUsers implements IUsers { public function getUserTOTPInfo(IUserInfo $userInfo): IUserTOTPInfo { - $stmt = $this->getStatement('get totp', fn() => ('SELECT ' . implode(',', self::TOTP_FIELDS) . ' FROM ' . self::TOTP_TABLE . ' WHERE user_id = ?')); - $stmt->reset(); + $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->execute(); $result = $stmt->getResult(); @@ -138,8 +136,7 @@ class DbUsers implements IUsers { public function getUserEMailInfos(IUserInfo $userInfo): array { - $stmt = $this->getStatement('get emails', fn() => ('SELECT ' . implode(',', self::EMAILS_FIELDS) . ' FROM ' . self::EMAILS_TABLE . ' WHERE user_id = ?')); - $stmt->reset(); + $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->execute(); $result = $stmt->getResult(); @@ -154,8 +151,7 @@ class DbUsers implements IUsers { public function getUserBackupInfos(IUserInfo $userInfo): array { - $stmt = $this->getStatement('get backups', fn() => ('SELECT ' . implode(',', self::BACKUP_FIELDS) . ' FROM ' . self::BACKUP_TABLE . ' WHERE user_id = ?')); - $stmt->reset(); + $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->execute(); $result = $stmt->getResult(); diff --git a/src/Users/IUserTOTPInfo.php b/src/Users/IUserTOTPInfo.php index f6bdb41..f332a8e 100644 --- a/src/Users/IUserTOTPInfo.php +++ b/src/Users/IUserTOTPInfo.php @@ -9,4 +9,5 @@ interface IUserTOTPInfo { public function getSecretKey(): string; public function getChangedTime(): DateTime; public function createGenerator(): TOTPGenerator; + public function generateValidCodes(int $offset = 0, int $timeStamp = -1): array; } diff --git a/src/Users/UserNotFoundException.php b/src/Users/UserNotFoundException.php new file mode 100644 index 0000000..69798d7 --- /dev/null +++ b/src/Users/UserNotFoundException.php @@ -0,0 +1,6 @@ +extends('auth/master'); + +$self->block('content', function() use ($self) { +?> +
+
+
+ +
+
+ +
+ +
+ +
+ authMethodNames)): ?> + Authenticator app (TOTP) + + authMethodNames)): ?> + Security key (U2F) + + authMethodNames)): ?> + Backup code + + Cancel +
+
+extends('auth/master'); + +$self->block('content', function() use ($self) { +?> +
+
+
+ +
+
+ +
+ + + +
+ +
+ + Back +
+
+extends('auth/master'); + +$self->registerSuffix = ''; +$self->forgotSuffix = ''; +if(!empty($self->userName)) { + $suffix = '?username=' . rawurlencode($self->userName); + + if($self->errorId === 'user_not_found') + $self->registerSuffix = $suffix; + else + $self->forgotSuffix = $suffix; +} + +$self->block('content', function() use ($self) { +?> +
+
+
+ +
+
+ +
+ + + +
+ + +
+extends('master'); + +$self->block('body', function() use ($self) { +?> +
+
+ hau->getSiteName();?> +
+ getBlock('content');?> +
+extends('master'); $self->block('body', function($self) { ?> -

http_error_title ?? 'Unknown Error');?>

-

http_error_desc ?? 'No additional information is available.');?>

+
+

http_error_title ?? 'Unknown Error');?>

+

http_error_desc ?? 'No additional information is available.');?>

+
<?=$self->hau->getSiteName();?> + getBlock('body');?> + diff --git a/tools/migrate b/tools/migrate old mode 100755 new mode 100644 diff --git a/tools/new-migration b/tools/new-migration old mode 100755 new mode 100644