Skip to content

Commit

Permalink
Flesh out password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
bakert committed Feb 14, 2024
1 parent b2eab7e commit 6a8615c
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 20 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"require": {
"wohali/oauth2-discord-new": "^1.0",
"sentry/sdk": "^4.0",
"ypho/scryfall": "^1.0"
"ypho/scryfall": "^1.0",
"ext-curl": "*",
"firebase/php-jwt": "^6.10"
},
"require-dev": {
"squizlabs/php_codesniffer": "3.*",
Expand Down
71 changes: 68 additions & 3 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions gatherling/config.php.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ $CONFIG['analytics_account'] = 'UA-37926034-1';
# How long to store session cookies in seconds
$CONFIG['cookie_lifetime'] = 5184000;

# API Key for Brevo email sending (password reset)
$CONFIG['brevo_api_key'] = 'xkeysib-foobar-baz';

# Encryption key for password reset JWT
$CONFIG['password_reset_key'] = 'foo-bar-baz';

# === Infobot PARAMETERS ==
# If you have set it up with infobot or PDBot to contact the site, you can change the passkey here.
# If you haven't set it up, leave this blank.
Expand Down
36 changes: 36 additions & 0 deletions gatherling/email.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

// Use Brevo to send an email from us to a single recipient. Brevo supports multiple recipients, attachments, etc. but we don't need that yet.
function sendEmail($to, $subj, $msg): bool {
global $CONFIG;

$body = [
'sender' => ['name' => 'Gatherling', 'email' => 'no-reply@gatherling.com'],
'to' => [['name' => $to, 'email' => $to]],
'subject' => $subj,
'htmlContent' => $msg
];

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.brevo.com/v3/smtp/email');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));

$headers = [];
$headers[] = 'Accept: application/json';
$headers[] = 'Api-Key: ' . $CONFIG['brevo_api_key'];
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

curl_exec($ch);
if (curl_errno($ch)) {
return false;
}
$response_code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($response_code >= 300) {
return false;
}
return true;
}
109 changes: 93 additions & 16 deletions gatherling/forgot.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
<?php

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

use Gatherling\Player;

include 'email.php';
include 'lib.php';
require_once 'lib_form_helper.php';

session_start();
print_header('Login');
printPageHeader();

if (isset($_POST['view']) && $_POST['view'] === 'send_login_link') {
if (isset($_POST['view']) && $_POST['view'] === 'new_password') {
if (resetPassword($_POST['token'], $_POST['password'])) {
echo '<p>Your password has been reset. You can now <a href="login.php">log in</a>.</p>';
} else {
echo '<p class="error">Unable to reset your password. Please try again.</p>';
printForgotForm();
}
} elseif (isset($_GET['token'])) {
printNewPasswordForm($_GET['token']);
} elseif (isset($_POST['view']) && $_POST['view'] === 'send_login_link') {
if (isset($_POST['identifier']) && strpos($_POST['identifier'], '@') !== false) {
$player = Player::findByEmail($_POST['identifier']);
} else {
Expand Down Expand Up @@ -54,21 +67,85 @@ function printPageFooter() {

function printForgotForm() {
?>
<p>Enter your email or username and we'll send you a link to get back into your account.</p>
<form action="forgot.php" method="post">
<input type="hidden" name="view" value="send_login_link" />
<table class="form">
<?php
print_text_input('Email or Username', 'identifier');
print_submit('Send Login Link');
?>
</table>
</form>
<p>If you aren't able to reset your password this way please message a Gatherling Administrator
on the <a href="https://discord.gg/2VJ8Fa6">Discord</a></p>
<?php
<p>Enter your email or username and we'll send you a link to get back into your account.</p>
<form action="forgot.php" method="post">
<input type="hidden" name="view" value="send_login_link" />
<table class="form">
<?php
print_text_input('Email or Username', 'identifier');
print_submit('Send Login Link');
?>
</table>
</form>
<p>If you aren't able to reset your password this way please message a Gatherling Administrator
on the <a href="https://discord.gg/2VJ8Fa6">Discord</a></p>
<?php
}

function sendLoginLink($player) {
return mail($player->emailAddress, 'Gatherling Login Link', "Click the following link to log in to Gatherling:\n\n");
function printNewPasswordForm($token) {
?>
<p>Enter your new password.</p>
<form action="forgot.php" method="post">
<input type="hidden" name="view" value="new_password" />
<input type="hidden" name="token" value="<?= htmlentities($token) ?>" />
<table class="form">
<?php
print_password_input('New Password', 'password');
print_submit('Reset Password');
?>
</table>
</form>
<?php
}

function sendLoginLink($player): bool {
$link = generateSecureResetLink($player->name);
$body = <<<END
<p>Hi $player->name,</p>
<p>Sorry to hear you’re having trouble logging into Gatherling. We got a message that you forgot your password.
If this was you, you can reset your password now.</p>
<p></p><a href="$link">Reset your password</a></p>
<p>If you didn’t request a login link or a password reset, you can ignore this message.</p>
<p>Only people who know your Gatherling password or click the link in this email can log into your account.</p>
END;
return sendEmail($player->emailAddress, 'Gatherling Login Link', $body);
}

function generateSecureResetLink($name): string {
global $CONFIG;

$key = $CONFIG['password_reset_key'];
$issuedAt = time();
$expirationTime = $issuedAt + 3600; // Token expires in 1 hour
$payload = [
'iss' => $CONFIG['base_url'], // Issuer
'aud' => $CONFIG['base_url'], // Audience
'iat' => $issuedAt, // Issued at
'exp' => $expirationTime, // Expiration time
'name' => $name // Also embed the player name, so we don't need to look up anything
];
$token = JWT::encode($payload, $key, 'HS256');
return $CONFIG['base_url'] . "/forgot.php?token=$token";
}

function resetPassword($token, $newPassword): bool {
global $CONFIG;

$payload = JWT::decode($token, new Key($CONFIG['password_reset_key'], 'HS256'));
if (!isset($payload->name) || !isset($payload->exp)) {
return false;
}
if (time() > $payload->exp) {
return false;
}
$player = Player::findByName($payload->name);
if (!$player) {
return false;
}
$player->setPassword($newPassword);
return true;
}

0 comments on commit 6a8615c

Please sign in to comment.