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;
|
namespace Misuzu;
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use Misuzu\Database\Database;
|
|
||||||
use Misuzu\Database\DatabaseMigrationManager;
|
|
||||||
use Misuzu\Net\GeoIP;
|
use Misuzu\Net\GeoIP;
|
||||||
use Misuzu\Net\IPAddress;
|
use Misuzu\Net\IPAddress;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
|
@ -80,7 +78,6 @@ require_once 'src/Forum/poll.php';
|
||||||
require_once 'src/Forum/post.php';
|
require_once 'src/Forum/post.php';
|
||||||
require_once 'src/Forum/topic.php';
|
require_once 'src/Forum/topic.php';
|
||||||
require_once 'src/Forum/validate.php';
|
require_once 'src/Forum/validate.php';
|
||||||
require_once 'src/Users/auth.php';
|
|
||||||
require_once 'src/Users/avatar.php';
|
require_once 'src/Users/avatar.php';
|
||||||
require_once 'src/Users/background.php';
|
require_once 'src/Users/background.php';
|
||||||
require_once 'src/Users/recovery.php';
|
require_once 'src/Users/recovery.php';
|
||||||
|
|
|
@ -5,6 +5,7 @@ use Misuzu\AuthToken;
|
||||||
use Misuzu\Net\IPAddress;
|
use Misuzu\Net\IPAddress;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
use Misuzu\Users\UserNotFoundException;
|
use Misuzu\Users\UserNotFoundException;
|
||||||
|
use Misuzu\Users\UserAuthSession;
|
||||||
use Misuzu\Users\UserLoginAttempt;
|
use Misuzu\Users\UserLoginAttempt;
|
||||||
use Misuzu\Users\UserSession;
|
use Misuzu\Users\UserSession;
|
||||||
use Misuzu\Users\UserSessionCreationFailedException;
|
use Misuzu\Users\UserSessionCreationFailedException;
|
||||||
|
@ -85,7 +86,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
||||||
|
|
||||||
if($userInfo->hasTOTP()) {
|
if($userInfo->hasTOTP()) {
|
||||||
url_redirect('auth-two-factor', [
|
url_redirect('auth-two-factor', [
|
||||||
'token' => user_auth_tfa_token_create($userInfo->getId()),
|
'token' => UserAuthSession::create($userInfo)->getToken(),
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ use Misuzu\Users\User;
|
||||||
use Misuzu\Users\UserLoginAttempt;
|
use Misuzu\Users\UserLoginAttempt;
|
||||||
use Misuzu\Users\UserSession;
|
use Misuzu\Users\UserSession;
|
||||||
use Misuzu\Users\UserSessionCreationFailedException;
|
use Misuzu\Users\UserSessionCreationFailedException;
|
||||||
|
use Misuzu\Users\UserAuthSession;
|
||||||
|
use Misuzu\Users\UserAuthSessionNotFoundException;
|
||||||
|
|
||||||
require_once '../../misuzu.php';
|
require_once '../../misuzu.php';
|
||||||
|
|
||||||
|
@ -18,13 +20,21 @@ $twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_PO
|
||||||
$notices = [];
|
$notices = [];
|
||||||
$ipAddress = IPAddress::remote();
|
$ipAddress = IPAddress::remote();
|
||||||
$remainingAttempts = UserLoginAttempt::remaining();
|
$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
|
// checking user_totp_key specifically because there's a fringe chance that
|
||||||
// there's a token present, but totp is actually disabled
|
// there's a token present, but totp is actually disabled
|
||||||
|
@ -63,7 +73,7 @@ while(!empty($twofactor)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserLoginAttempt::create(true, $userInfo);
|
UserLoginAttempt::create(true, $userInfo);
|
||||||
user_auth_tfa_token_invalidate($tokenInfo['tfa_token']);
|
$tokenInfo->delete();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$sessionInfo = UserSession::create($userInfo);
|
$sessionInfo = UserSession::create($userInfo);
|
||||||
|
@ -88,5 +98,5 @@ Template::render('auth.twofactor', [
|
||||||
'twofactor_notices' => $notices,
|
'twofactor_notices' => $notices,
|
||||||
'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : url('index'),
|
'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : url('index'),
|
||||||
'twofactor_attempts_remaining' => $remainingAttempts,
|
'twofactor_attempts_remaining' => $remainingAttempts,
|
||||||
'twofactor_token' => $tokenInfo['tfa_token'],
|
'twofactor_token' => $tokenInfo->getToken(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
use Misuzu\AuditLog;
|
use Misuzu\AuditLog;
|
||||||
|
use Misuzu\Config;
|
||||||
use Misuzu\Users\User;
|
use Misuzu\Users\User;
|
||||||
use Misuzu\Users\UserSession;
|
use Misuzu\Users\UserSession;
|
||||||
use chillerlan\QRCode\QRCode;
|
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($isVerifiedRequest && isset($_POST['tfa']['enable']) && (bool)$twoFactorInfo['totp_enabled'] !== (bool)$_POST['tfa']['enable']) {
|
||||||
if((bool)$_POST['tfa']['enable']) {
|
if((bool)$_POST['tfa']['enable']) {
|
||||||
$tfaKey = TOTP::generateKey();
|
$tfaKey = TOTP::generateKey();
|
||||||
|
$tfaIssuer = Config::get('site.name', Config::TYPE_STR, 'Misuzu');
|
||||||
$tfaQrcode = (new QRCode(new QROptions([
|
$tfaQrcode = (new QRCode(new QROptions([
|
||||||
'version' => 5,
|
'version' => 5,
|
||||||
'outputType' => QRCode::OUTPUT_IMAGE_JPG,
|
'outputType' => QRCode::OUTPUT_IMAGE_JPG,
|
||||||
'eccLevel' => QRCode::ECC_L,
|
'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,
|
'secret' => $tfaKey,
|
||||||
'issuer' => 'Flashii',
|
'issuer' => $tfaIssuer,
|
||||||
])));
|
])));
|
||||||
|
|
||||||
Template::set([
|
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;
|
private $user = null;
|
||||||
|
|
||||||
|
public const TOKEN_WIDTH = 32;
|
||||||
public const TOKEN_LIFETIME = 60 * 60 * 24 * 7;
|
public const TOKEN_LIFETIME = 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
public const TABLE = 'user_chat_tokens';
|
public const TABLE = 'user_chat_tokens';
|
||||||
|
@ -56,7 +57,7 @@ class UserChatToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function generateToken(): string {
|
public static function generateToken(): string {
|
||||||
return bin2hex(random_bytes(32));
|
return bin2hex(random_bytes(self::TOKEN_WIDTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function create(User $user): self {
|
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