misuzu/public/auth.php

263 lines
9.6 KiB
PHP

<?php
use Carbon\Carbon;
use Misuzu\Database;
use Misuzu\Net\IPAddress;
use Misuzu\Users\Session;
require_once __DIR__ . '/../misuzu.php';
$db = Database::connection();
$config = $app->getConfig();
$templating = $app->getTemplating();
$username_validation_errors = [
'trim' => 'Your username may not start or end with spaces!',
'short' => "Your username is too short, it has to be at least " . MSZ_USERNAME_MIN_LENGTH . " characters!",
'long' => "Your username is too long, it can't be longer than " . MSZ_USERNAME_MAX_LENGTH . " characters!",
'double-spaces' => "Your username can't contain double spaces.",
'invalid' => 'Your username contains invalid characters.',
'spacing' => 'Please use either underscores or spaces, not both!',
'in-use' => 'This username is already taken!',
];
$mode = $_GET['m'] ?? 'login';
$prevent_registration = $config->get('Auth', 'prevent_registration', 'bool', false);
$templating->var('auth_mode', $mode);
$templating->addPath('auth', __DIR__ . '/../views/auth');
$templating->var('prevent_registration', $prevent_registration);
if (!empty($_REQUEST['username'])) {
$templating->var('auth_username', $_REQUEST['username']);
}
if (!empty($_REQUEST['email'])) {
$templating->var('auth_email', $_REQUEST['email']);
}
switch ($mode) {
case 'logout':
if (!$app->hasActiveSession()) {
header('Location: /');
return;
}
if (isset($_GET['s']) && tmp_csrf_verify($_GET['s'])) {
set_cookie_m('uid', '', -3600);
set_cookie_m('sid', '', -3600);
$deleteSession = $db->prepare('
DELETE FROM `msz_sessions`
WHERE `session_id` = :session_id
');
$deleteSession->bindValue('session_id', $app->getSessionId());
$deleteSession->execute();
header('Location: /');
return;
}
echo $templating->render('@auth.logout');
break;
case 'login':
if ($app->hasActiveSession()) {
header('Location: /');
break;
}
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$auth_login_error = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ipAddressObj = IPAddress::remote();
$ipAddress = $ipAddressObj->getString();
if (!isset($_POST['username'], $_POST['password'])) {
$auth_login_error = "You didn't fill all the forms!";
break;
}
$fetchRemainingAttempts = $db->prepare('
SELECT 5 - COUNT(`attempt_id`)
FROM `msz_login_attempts`
WHERE `was_successful` = false
AND `created_at` > NOW() - INTERVAL 1 HOUR
AND `attempt_ip` = INET6_ATON(:remote_ip)
');
$fetchRemainingAttempts->bindValue('remote_ip', $ipAddress);
$remainingAttempts = $fetchRemainingAttempts->execute()
? (int)$fetchRemainingAttempts->fetchColumn()
: 0;
if ($remainingAttempts < 1) {
$auth_login_error = 'Too many failed login attempts, try again later.';
break;
}
$remainingAttempts -= 1;
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$getUser = $db->prepare('
SELECT `user_id`, `password`
FROM `msz_users`
WHERE LOWER(`email`) = LOWER(:email)
OR LOWER(`username`) = LOWER(:username)
');
$getUser->bindValue('email', $username);
$getUser->bindValue('username', $username);
$userData = $getUser->execute() ? $getUser->fetch() : [];
$userId = (int)($userData['user_id'] ?? 0);
$auth_error_str = "Invalid username or password, {$remainingAttempts} attempt(s) remaining.";
if ($userId < 1) {
user_login_attempt_record(false, null, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
break;
}
if (!password_verify($password, $userData['password'])) {
user_login_attempt_record(false, $userId, $ipAddress, $user_agent);
$auth_login_error = $auth_error_str;
break;
}
user_login_attempt_record(true, $userId, $ipAddress, $user_agent);
$sessionKey = bin2hex(random_bytes(32));
$createSession = $db->prepare('
INSERT INTO `msz_sessions`
(`user_id`, `session_ip`, `user_agent`, `session_key`, `created_at`, `expires_on`)
VALUES
(:user_id, INET6_ATON(:session_ip), :user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH)
');
$createSession->bindValue('user_id', $userId);
$createSession->bindValue('session_ip', $ipAddress);
$createSession->bindValue('user_agent', $user_agent);
$createSession->bindValue('session_key', $sessionKey);
if (!$createSession->execute()) {
$auth_login_error = 'Unable to create new session, contact an administrator.';
break;
}
$app->startSession($userId, $sessionKey);
$cookieLife = Carbon::now()->addMonth()->timestamp;
set_cookie_m('uid', $userId, $cookieLife);
set_cookie_m('sid', $sessionKey, $cookieLife);
// Temporary key generation for chat login.
// Should eventually be replaced with a callback login system.
// Also uses different cookies since $httponly is required to be false for these.
$chatKey = bin2hex(random_bytes(16));
$setChatKey = $db->prepare('
UPDATE `msz_users`
SET `user_chat_key` = :user_chat_key
WHERE `user_id` = :user_id
');
$setChatKey->bindValue('user_chat_key', $chatKey);
$setChatKey->bindValue('user_id', $userId);
if ($setChatKey->execute()) {
setcookie('msz_tmp_id', $userId, $cookieLife, '/', '.flashii.net');
setcookie('msz_tmp_key', $chatKey, $cookieLife, '/', '.flashii.net');
}
header('Location: /');
return;
}
if (!empty($auth_login_error)) {
$templating->var('auth_login_error', $auth_login_error);
}
echo $templating->render('auth');
break;
case 'register':
if ($app->hasActiveSession()) {
header('Location: /');
}
$auth_register_error = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($prevent_registration) {
$auth_register_error = 'Registration is not allowed on this instance.';
break;
}
if (!isset($_POST['username'], $_POST['password'], $_POST['email'])) {
$auth_register_error = "You didn't fill all the forms!";
break;
}
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
$username_validate = user_validate_username($username, true);
if ($username_validate !== '') {
$auth_register_error = $username_validation_errors[$username_validate];
break;
}
$email_validate = user_validate_email($email, true);
if ($email_validate !== '') {
$auth_register_error = $email_validate === 'in-use'
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
break;
}
if (user_validate_password($password) !== '') {
$auth_register_error = 'Your password is too weak!';
break;
}
$ipAddress = IPAddress::remote()->getString();
$createUser = $db->prepare('
INSERT INTO `msz_users`
(
`username`, `password`, `email`, `register_ip`,
`last_ip`, `user_country`, `created_at`, `display_role`
)
VALUES
(
:username, :password, :email, INET6_ATON(:register_ip),
INET6_ATON(:last_ip), :user_country, NOW(), 1
)
');
$createUser->bindValue('username', $username);
$createUser->bindValue('password', password_hash($password, PASSWORD_ARGON2I));
$createUser->bindValue('email', $email);
$createUser->bindValue('register_ip', $ipAddress);
$createUser->bindValue('last_ip', $ipAddress);
$createUser->bindValue('user_country', get_country_code($ipAddress));
if (!$createUser->execute()) {
$auth_register_error = 'Something happened?';
break;
}
$addRole = $db->prepare('
INSERT INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, 1)
');
$addRole->bindValue('user_id', $db->lastInsertId());
$addRole->execute();
$templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break;
}
if (!empty($auth_register_error)) {
$templating->var('auth_register_error', $auth_register_error);
}
echo $templating->render('@auth.auth');
break;
}