Made two factor auth session code OOP.
This commit is contained in:
parent
4657bfa548
commit
8bd45209a2
7 changed files with 119 additions and 65 deletions
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
]);
|
||||
|
|
|
@ -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([
|
||||
|
|
93
src/Users/UserAuthSession.php
Normal file
93
src/Users/UserAuthSession.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
Loading…
Add table
Reference in a new issue