misuzu/src/Auth/RecoveryTokens.php

106 lines
3.7 KiB
PHP

<?php
namespace Misuzu\Auth;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Index\Serialisation\Base32;
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\User;
class RecoveryTokens {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
private static function generateCode(): string {
// results in a 12 char code
return Base32::encode(random_bytes(7));
}
public function getToken(
User|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
?string $verifyCode = null,
?bool $isUnused = null
): RecoveryTokenInfo {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
$hasVerifyCode = $verifyCode !== null;
$hasIsUnused = $isUnused !== null;
if(!$hasUserInfo && !$hasRemoteAddr && !$hasVerifyCode)
throw new InvalidArgumentException('At least one argument must be correctly specified.');
if($hasIsUnused && $hasVerifyCode)
throw new InvalidArgumentException('You may not specify $isUnused and $verifyCode at the same time.');
$args = 0;
$query = 'SELECT user_id, INET6_NTOA(reset_ip), UNIX_TIMESTAMP(reset_requested), verification_code FROM msz_users_password_resets';
if($hasUserInfo) {
++$args;
$query .= ' WHERE user_id = ?';
}
if($hasRemoteAddr)
$query .= sprintf(' %s reset_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
if($hasIsUnused)
$query .= sprintf(' %s verification_code %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $isUnused ? 'IS NOT' : 'IS');
elseif($hasVerifyCode)
$query .= sprintf(' %s verification_code = ?', ++$args > 1 ? 'AND' : 'WHERE');
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasRemoteAddr)
$stmt->addParameter(++$args, $remoteAddr);
if($hasVerifyCode)
$stmt->addParameter(++$args, $verifyCode);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Recovery token not found.');
return new RecoveryTokenInfo($result);
}
public function createToken(
User|string $userInfo,
IPAddress|string $remoteAddr
): RecoveryTokenInfo {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$verifyCode = self::generateCode();
$stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $remoteAddr);
$stmt->addParameter(3, $verifyCode);
$stmt->execute();
return $this->getToken(verifyCode: $verifyCode);
}
public function invalidateToken(RecoveryTokenInfo $tokenInfo): void {
if(!$tokenInfo->hasCode())
return;
$stmt = $this->cache->get('UPDATE msz_users_password_resets SET verification_code = NULL WHERE verification_code = ? AND user_id = ?');
$stmt->addParameter(1, $tokenInfo->getCode());
$stmt->addParameter(2, $tokenInfo->getUserId());
$stmt->execute();
}
}