Made two factor auth session code OOP.

This commit is contained in:
flash 2020-05-28 17:52:31 +00:00
parent 4657bfa548
commit 8bd45209a2
7 changed files with 119 additions and 65 deletions

View file

@ -2,8 +2,6 @@
namespace Misuzu;
use PDO;
use Misuzu\Database\Database;
use Misuzu\Database\DatabaseMigrationManager;
use Misuzu\Net\GeoIP;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
@ -80,7 +78,6 @@ require_once 'src/Forum/poll.php';
require_once 'src/Forum/post.php';
require_once 'src/Forum/topic.php';
require_once 'src/Forum/validate.php';
require_once 'src/Users/auth.php';
require_once 'src/Users/avatar.php';
require_once 'src/Users/background.php';
require_once 'src/Users/recovery.php';

View file

@ -5,6 +5,7 @@ use Misuzu\AuthToken;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserAuthSession;
use Misuzu\Users\UserLoginAttempt;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserSessionCreationFailedException;
@ -85,7 +86,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
if($userInfo->hasTOTP()) {
url_redirect('auth-two-factor', [
'token' => user_auth_tfa_token_create($userInfo->getId()),
'token' => UserAuthSession::create($userInfo)->getToken(),
]);
return;
}

View file

@ -6,6 +6,8 @@ use Misuzu\Users\User;
use Misuzu\Users\UserLoginAttempt;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserSessionCreationFailedException;
use Misuzu\Users\UserAuthSession;
use Misuzu\Users\UserAuthSessionNotFoundException;
require_once '../../misuzu.php';
@ -18,13 +20,21 @@ $twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_PO
$notices = [];
$ipAddress = IPAddress::remote();
$remainingAttempts = UserLoginAttempt::remaining();
$tokenInfo = user_auth_tfa_token_info(
!empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
!empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
)
);
$userInfo = User::byId($tokenInfo['user_id']);
try {
$tokenInfo = UserAuthSession::byToken(
!empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
!empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
)
);
} catch(UserAuthSessionNotFoundException $ex) {}
if(empty($tokenInfo) || $tokenInfo->hasExpired()) {
url_redirect('auth-login');
return;
}
$userInfo = $tokenInfo->getUser();
// checking user_totp_key specifically because there's a fringe chance that
// there's a token present, but totp is actually disabled
@ -63,7 +73,7 @@ while(!empty($twofactor)) {
}
UserLoginAttempt::create(true, $userInfo);
user_auth_tfa_token_invalidate($tokenInfo['tfa_token']);
$tokenInfo->delete();
try {
$sessionInfo = UserSession::create($userInfo);
@ -88,5 +98,5 @@ Template::render('auth.twofactor', [
'twofactor_notices' => $notices,
'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : url('index'),
'twofactor_attempts_remaining' => $remainingAttempts,
'twofactor_token' => $tokenInfo['tfa_token'],
'twofactor_token' => $tokenInfo->getToken(),
]);

View file

@ -2,6 +2,7 @@
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Config;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use chillerlan\QRCode\QRCode;
@ -46,13 +47,14 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
if($isVerifiedRequest && isset($_POST['tfa']['enable']) && (bool)$twoFactorInfo['totp_enabled'] !== (bool)$_POST['tfa']['enable']) {
if((bool)$_POST['tfa']['enable']) {
$tfaKey = TOTP::generateKey();
$tfaIssuer = Config::get('site.name', Config::TYPE_STR, 'Misuzu');
$tfaQrcode = (new QRCode(new QROptions([
'version' => 5,
'outputType' => QRCode::OUTPUT_IMAGE_JPG,
'eccLevel' => QRCode::ECC_L,
])))->render(sprintf('otpauth://totp/Flashii:%s?%s', $twoFactorInfo['username'], http_build_query([
])))->render(sprintf('otpauth://totp/%s:%s?%s', $tfaIssuer, $twoFactorInfo['username'], http_build_query([
'secret' => $tfaKey,
'issuer' => 'Flashii',
'issuer' => $tfaIssuer,
])));
Template::set([

View file

@ -0,0 +1,93 @@
<?php
namespace Misuzu\Users;
use Misuzu\DB;
class UserAuthSessionException extends UsersException {}
class UserAuthSessionNotFoundException extends UserAuthSessionException {}
class UserAuthSessionCreationFailedException extends UserAuthSessionException {}
class UserAuthSession {
// Database fields
private $user_id = -1;
private $tfa_token = '';
private $tfa_created = null;
private $user = null;
public const TOKEN_WIDTH = 16;
public const TOKEN_LIFETIME = 60 * 15;
public const TABLE = 'auth_tfa';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`user_id`, %1$s.`tfa_token`'
. ', UNIX_TIMESTAMP(%1$s.`tfa_created`) AS `tfa_created`';
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function getUser(): User {
if($this->user === null)
$this->user = User::byId($this->getUserId());
return $this->user;
}
public function getToken(): string {
return $this->tfa_token;
}
public function getCreationTime(): int {
return $this->tfa_created === null ? -1 : $this->tfa_created;
}
public function getExpirationTime(): int {
return $this->getCreationTime() + self::TOKEN_LIFETIME;
}
public function hasExpired(): bool {
return $this->getExpirationTime() <= time();
}
public function delete(): void {
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `tfa_token` = :token')
->bind('token', $this->tfa_token)
->execute();
}
public static function generateToken(): string {
return bin2hex(random_bytes(self::TOKEN_WIDTH));
}
public static function create(User $user, bool $return = true): ?self {
$token = self::generateToken();
$created = DB::prepare('INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `tfa_token`) VALUES (:user, :token)')
->bind('user', $user->getId())
->bind('token', $token)
->execute();
if(!$created)
throw new UserAuthSessionCreationFailedException;
if(!$return)
return null;
try {
$object = self::byToken($token);
$object->user = $user;
return $object;
} catch(UserAuthSessionNotFoundException $ex) {
throw new UserAuthSessionCreationFailedException;
}
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byToken(string $token): self {
$object = DB::prepare(self::byQueryBase() . ' WHERE `tfa_token` = :token')
->bind('token', $token)
->fetchObject(self::class);
if(!$object)
throw new UserAuthSessionNotFoundException;
return $object;
}
}

View file

@ -15,6 +15,7 @@ class UserChatToken {
private $user = null;
public const TOKEN_WIDTH = 32;
public const TOKEN_LIFETIME = 60 * 60 * 24 * 7;
public const TABLE = 'user_chat_tokens';
@ -56,7 +57,7 @@ class UserChatToken {
}
public static function generateToken(): string {
return bin2hex(random_bytes(32));
return bin2hex(random_bytes(self::TOKEN_WIDTH));
}
public static function create(User $user): self {

View file

@ -1,50 +0,0 @@
<?php
define('MSZ_AUTH_TFA_TOKENS_SIZE', 16); // * 2
function user_auth_tfa_token_generate(): string {
return bin2hex(random_bytes(MSZ_AUTH_TFA_TOKENS_SIZE));
}
function user_auth_tfa_token_create(int $userId): string {
if($userId < 1)
return '';
$token = user_auth_tfa_token_generate();
$createToken = \Misuzu\DB::prepare('
INSERT INTO `msz_auth_tfa`
(`user_id`, `tfa_token`)
VALUES
(:user_id, :token)
');
$createToken->bind('user_id', $userId);
$createToken->bind('token', $token);
if(!$createToken->execute())
return '';
return $token;
}
function user_auth_tfa_token_invalidate(string $token): void {
$deleteToken = \Misuzu\DB::prepare('
DELETE FROM `msz_auth_tfa`
WHERE `tfa_token` = :token
');
$deleteToken->bind('token', $token);
$deleteToken->execute();
}
function user_auth_tfa_token_info(string $token): array {
$getTokenInfo = \Misuzu\DB::prepare('
SELECT
at.`user_id`, at.`tfa_token`, at.`tfa_created`, u.`user_totp_key`
FROM `msz_auth_tfa` AS at
LEFT JOIN `msz_users` AS u
ON u.`user_id` = at.`user_id`
WHERE at.`tfa_token` = :token
AND at.`tfa_created` >= NOW() - INTERVAL 15 MINUTE
');
$getTokenInfo->bind('token', $token);
return $getTokenInfo->fetch();
}