Added password resetting.

This commit is contained in:
flash 2018-07-22 01:54:12 +02:00
parent 7c1dd2ed7d
commit dc79fcb2f8
7 changed files with 251 additions and 19 deletions

View file

@ -84,6 +84,7 @@
&--message {
padding-left: 10px;
color: #aaa;
max-width: 220px;
}
&--error {

View file

@ -14,4 +14,9 @@
&__label {
display: block;
}
&--disabled {
// same colour as placeholder (in firefox)
color: #959595;
}
}

View file

@ -82,6 +82,12 @@ if (PHP_SAPI === 'cli') {
WHERE `expires_on` < NOW()
');
// Remove old password reset records, left for a week for possible review
Database::exec('
DELETE FROM `msz_users_password_resets`
WHERE `reset_requested` < NOW() - INTERVAL 1 WEEK
');
// Cleans up the login history table
Database::exec('
DELETE FROM `msz_login_attempts`

View file

@ -1,5 +1,6 @@
<?php
use Carbon\Carbon;
use Misuzu\Application;
use Misuzu\Database;
use Misuzu\Net\IPAddress;
use Misuzu\Users\Session;
@ -7,7 +8,7 @@ use Misuzu\Users\Session;
require_once __DIR__ . '/../misuzu.php';
$config = $app->getConfig();
$templating = $app->getTemplating();
$tpl = $app->getTemplating();
$usernameValidationErrors = [
'trim' => 'Your username may not start or end with spaces!',
@ -21,9 +22,9 @@ $usernameValidationErrors = [
$authMode = $_GET['m'] ?? 'login';
$preventRegistration = $config->get('Auth', 'prevent_registration', 'bool', false);
$templating->addPath('auth', __DIR__ . '/../views/auth');
$tpl->addPath('auth', __DIR__ . '/../views/auth');
$templating->vars([
$tpl->vars([
'prevent_registration' => $preventRegistration,
'auth_mode' => $authMode,
'auth_username' => $_REQUEST['username'] ?? '',
@ -45,7 +46,182 @@ switch ($authMode) {
return;
}
echo $templating->render('@auth.logout');
echo $tpl->render('@auth.logout');
break;
case 'reset':
if ($app->hasActiveSession()) {
header('Location: /settings.php');
break;
}
$resetUser = (int)($_POST['user'] ?? $_GET['u'] ?? 0);
$getResetUser = Database::prepare('
SELECT `user_id`, `username`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$getResetUser->bindValue('user_id', $resetUser);
$resetUser = $getResetUser->execute() ? $getResetUser->fetch(PDO::FETCH_ASSOC) : [];
if (empty($resetUser)) {
header('Location: ?m=forgot');
break;
}
$tpl->var('auth_reset_message', 'A verification code should\'ve been sent to your e-mail address.');
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$validateRequest = Database::prepare('
SELECT COUNT(`user_id`) > 0
FROM `msz_users_password_resets`
WHERE `user_id` = :user
AND `verification_code` = :code
AND `verification_code` IS NOT NULL
AND `reset_requested` > NOW() - INTERVAL 1 HOUR
');
$validateRequest->bindValue('user', $resetUser['user_id']);
$validateRequest->bindValue('code', $_POST['verification'] ?? '');
$validateRequest = $validateRequest->execute()
? (bool)$validateRequest->fetchColumn()
: false;
if (!$validateRequest) {
$tpl->var('auth_reset_error', 'Invalid verification code!');
break;
}
$tpl->var('reset_verify', $_POST['verification']);
if (empty($_POST['password']['new'])
|| empty($_POST['password']['confirm'])
|| $_POST['password']['new'] !== $_POST['password']['confirm']) {
$tpl->var('auth_reset_error', 'Your passwords didn\'t match!');
break;
}
if (user_validate_password($_POST['password']['new']) !== '') {
$tpl->var('auth_reset_error', 'Your password is too weak!');
break;
}
$updatePassword = Database::prepare('
UPDATE `msz_users`
SET `password` = :password
WHERE `user_id` = :user
');
$updatePassword->bindValue('user', $resetUser['user_id']);
$updatePassword->bindValue('password', user_password_hash($_POST['password']['new']));
if ($updatePassword->execute()) {
audit_log('PASSWORD_RESET', $resetUser['user_id']);
} else {
throw new UnexpectedValueException('Password reset failed.');
}
$invalidateCode = Database::prepare('
UPDATE `msz_users_password_resets`
SET `verification_code` = NULL
WHERE `verification_code` = :code
AND `user_id` = :user
');
$invalidateCode->bindValue('user', $resetUser['user_id']);
$invalidateCode->bindValue('code', $_POST['verification']);
if (!$invalidateCode->execute()) {
throw new UnexpectedValueException('Verification code invalidation failed.');
}
header('Location: /auth.php?m=login');
break;
}
echo $tpl->render('@auth.password', [
'reset_user' => $resetUser,
]);
break;
case 'forgot':
if ($app->hasActiveSession()) {
header('Location: /');
break;
}
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($_POST['email'])) {
$tpl->var('auth_forgot_error', 'Please enter an e-mail address.');
break;
}
$forgotUser = Database::prepare('
SELECT `user_id`, `username`, `email`
FROM `msz_users`
WHERE LOWER(`email`) = LOWER(:email)
');
$forgotUser->bindValue('email', $_POST['email']);
$forgotUser = $forgotUser->execute() ? $forgotUser->fetch(PDO::FETCH_ASSOC) : [];
if (empty($forgotUser)) {
break;
}
$ipAddress = IPAddress::remote()->getString();
$emailSent = Database::prepare('
SELECT COUNT(`verification_code`) > 0
FROM `msz_users_password_resets`
WHERE `user_id` = :user
AND `reset_ip` = INET6_ATON(:ip)
AND `reset_requested` > NOW() - INTERVAL 1 HOUR
AND `verification_code` IS NOT NULL
');
$emailSent->bindValue('user', $forgotUser['user_id']);
$emailSent->bindValue('ip', $ipAddress);
$emailSent = $emailSent->execute()
? (bool)$emailSent->fetchColumn()
: false;
if (!$emailSent) {
$verificationCode = bin2hex(random_bytes(6));
$insertResetKey = Database::prepare('
REPLACE INTO `msz_users_password_resets`
(`user_id`, `reset_ip`, `verification_code`)
VALUES
(:user, INET6_ATON(:ip), :code)
');
$insertResetKey->bindValue('user', $forgotUser['user_id']);
$insertResetKey->bindValue('ip', $ipAddress);
$insertResetKey->bindValue('code', $verificationCode);
if (!$insertResetKey->execute()) {
throw new UnexpectedValueException('A verification code failed to insert.');
}
$messageBody = <<<MSG
Hey {$forgotUser['username']},
You, or someone pretending to be you, has requested a password reset for your account.
Your verification code is: {$verificationCode}
If you weren't the person who requested this reset, please send a reply to this e-mail.
MSG;
$message = (new Swift_Message('Flashii Password Reset'))
->setFrom([
$config->get('Mail', 'sender_email', 'string', 'sys@misuzu.lh') =>
$config->get('Mail', 'sender_name', 'string', 'Misuzu')
])
->setTo([$forgotUser['email'] => $forgotUser['username']])
->setBody($messageBody);
Application::mailer()->send($message);
}
header("Location: ?m=reset&u={$forgotUser['user_id']}");
break;
}
echo $tpl->render('@auth.auth');
break;
case 'login':
@ -122,10 +298,10 @@ switch ($authMode) {
}
if (!empty($authLoginError)) {
$templating->var('auth_login_error', $authLoginError);
$tpl->var('auth_login_error', $authLoginError);
}
echo $templating->render('auth');
echo $tpl->render('@auth.auth');
break;
case 'register':
@ -183,14 +359,14 @@ switch ($authMode) {
user_role_add($createUser, MSZ_ROLE_MAIN);
$templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
$tpl->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
break;
}
if (!empty($authRegistrationError)) {
$templating->var('auth_register_error', $authRegistrationError);
$tpl->var('auth_register_error', $authRegistrationError);
}
echo $templating->render('@auth.auth');
echo $tpl->render('@auth.auth');
break;
}

View file

@ -10,13 +10,16 @@
<label class="form__row">
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
</label>
<label class="form__row">
<input class="text text--password" type="password" name="password" placeholder="password" required>
</label>
<div class="form__row form__row--columns">
<div class="form__column form__column--message{% if auth_login_error is defined %} form__column--error{% endif %}">
{{ auth_login_message|default(auth_login_error|default('')) }}
</div>
<div class="form__column">
<button class="button button--login">Login</button>
</div>
@ -38,12 +41,15 @@
<label class="form__row">
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
</label>
<label class="form__row">
<input class="text text--password" type="password" name="password" placeholder="password" required>
</label>
<label class="form__row">
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
</label>
<div class="form__row form__row--columns">
<div class="form__column form__column--message{% if auth_register_error is defined %} form__column--error{% endif %}">
{{ auth_register_message|default(auth_register_error|default('')) }}
@ -57,24 +63,25 @@
</div>
</div>
{# <div class="form__wrapper">
<input class="form__toggle" id="_authmode_restore" type="radio" name="_authmode"{% if auth_mode == 'restore' %} checked{% endif %}>
<div class="form form--restore">
<label class="form__title" for="_authmode_restore">Forgot login details</label>
<form class="form__content" method="post" action="?m=restore">
<div class="form__wrapper">
<input class="form__toggle" id="_authmode_forgot" type="radio" name="_authmode"{% if auth_mode == 'forgot' %} checked{% endif %}>
<div class="form form--forgot">
<label class="form__title" for="_authmode_forgot">Forgot your password</label>
<form class="form__content" method="post" action="?m=forgot">
<label class="form__row">
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
</label>
<div class="form__row form__row--columns">
<div class="form__column form__column--message{% if auth_restore_error is defined %} form__column--error{% endif %}">
{{ auth_restore_message|default(auth_restore_error|default('')) }}
<div class="form__column form__column--message{% if auth_forgot_error is defined %} form__column--error{% endif %}">
{{ auth_forgot_message|default(auth_forgot_error|default('')) }}
</div>
<div class="form__column">
<button class="button button--restore">Send reminder</button>
<button class="button button--forgot">Send reminder</button>
</div>
</div>
</form>
</div>
</div> #}
</div>
</div>
{% endblock %}

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<title>Authentication</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/auth.css" rel="stylesheet" type="text/css">
<link href="{{ '/css/auth.css'|asset_url }}" rel="stylesheet" type="text/css">
</head>
<body>
<div class="auth">

37
views/auth/password.twig Normal file
View file

@ -0,0 +1,37 @@
{% extends '@auth/master.twig' %}
{% block content %}
<div class="container">
<div class="form__wrapper">
<input class="form__toggle" id="_authmode_reset" type="radio" name="_authmode" checked>
<div class="form form--reset">
<label class="form__title" for="_authmode_reset">Resetting password for {{ reset_user.username }}</label>
<form class="form__content" method="post" action="?m=reset">
<input type="hidden" name="user" value="{{ reset_user.user_id }}">
<label class="form__row">
<input class="text text--password" type="text" name="verification" placeholder="verification code" maxlength="12" value="{{ reset_verify|default('') }}" required>
</label>
<label class="form__row">
<input class="text text--password" type="password" name="password[new]" placeholder="new password" required>
</label>
<label class="form__row">
<input class="text text--password" type="password" name="password[confirm]" placeholder="confirm password" required>
</label>
<div class="form__row form__row--columns">
<div class="form__column form__column--message{% if auth_reset_error is defined %} form__column--error{% endif %}">
{{ auth_reset_error|default(auth_reset_message|default('')) }}
</div>
<div class="form__column">
<button class="button button--change">Change password</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}