diff --git a/composer.json b/composer.json index 532f92f61..de2e5a654 100644 --- a/composer.json +++ b/composer.json @@ -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.*", diff --git a/composer.lock b/composer.lock index 9257dc282..4aa50c4b3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,71 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "26738de395bb8728d6df2a2168699819", + "content-hash": "dc27b748893ac72f40db5939ba800294", "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + }, + "time": "2023-12-01T16:26:39+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.5.8", @@ -3622,7 +3685,9 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-curl": "*" + }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/gatherling/config.php.example b/gatherling/config.php.example index 6ce331656..496d90f59 100644 --- a/gatherling/config.php.example +++ b/gatherling/config.php.example @@ -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. diff --git a/gatherling/email.php b/gatherling/email.php new file mode 100644 index 000000000..37914711e --- /dev/null +++ b/gatherling/email.php @@ -0,0 +1,38 @@ + ['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; +} diff --git a/gatherling/forgot.php b/gatherling/forgot.php index f82906372..e900631ad 100644 --- a/gatherling/forgot.php +++ b/gatherling/forgot.php @@ -1,7 +1,11 @@ Your password has been reset. You can now log in.
'; + } else { + echo 'Unable to reset your password. Please try again.
'; + 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 { @@ -54,21 +67,85 @@ function printPageFooter() { function printForgotForm() { ?> -Enter your email or username and we'll send you a link to get back into your account.
- -If you aren't able to reset your password this way please message a Gatherling Administrator - on the Discord
-Enter your email or username and we'll send you a link to get back into your account. + +If you aren't able to reset your password this way please message a Gatherling Administrator + on the Discord
+ emailAddress, 'Gatherling Login Link', "Click the following link to log in to Gatherling:\n\n"); +function printNewPasswordForm($token) { + ?> +Enter your new password.
+ + name); + $body = <<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.
+ + Reset your password + +If you didn’t request a login link or a password reset, you can ignore this message.
+ +Only people who know your Gatherling password or click the link in this email can log into your account.
+ 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; }