Skip to content

Instantly share code, notes, and snippets.

@SoftCreatR
Created November 19, 2024 08:33
Show Gist options
  • Save SoftCreatR/0bb999089ca49368aed39e97d1cd8bb0 to your computer and use it in GitHub Desktop.
Save SoftCreatR/0bb999089ca49368aed39e97d1cd8bb0 to your computer and use it in GitHub Desktop.
PHP script that securely processes a Facebook-signed request to reset a WoltLab Suite user's password by verifying the signature, updating user data with a reset key, and sending a password reset email.
<?php
/*
* Copyright by SoftCreatR.dev.
*
* License: https://softcreatr.dev/license-terms
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* The above copyright notice and this disclaimer notice shall be included in all
* copies or substantial portions of the Software.
*/
use wcf\data\user\User;
use wcf\data\user\UserAction;
use wcf\system\email\Email;
use wcf\system\email\mime\MimePartFacade;
use wcf\system\email\mime\RecipientAwareTextMimePart;
use wcf\system\email\UserMailbox;
use wcf\system\exception\SystemException;
use wcf\util\JSON;
use function wcf\functions\exception\logThrowable;
// Include WSC
require __DIR__ . '/global.php';
/**
* Decode a Base64 URL-encoded string.
*
* @throws SystemException If decoding fails.
*/
function base64UrlDecode(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
$input .= \str_repeat('=', 4 - $remainder);
}
$decoded = \base64_decode(\strtr($input, '-_', '+/'), true);
if ($decoded === false) {
throw new SystemException('Base64 decoding failed.');
}
return $decoded;
}
/**
* Parse and verify a signed request.
*
* @throws SystemException If the signed request is invalid.
*/
function parseSignedRequest(string $signedRequest, string $secret): array
{
$parts = \explode('.', $signedRequest, 2);
if (\count($parts) !== 2) {
throw new SystemException('Signed request format is invalid.');
}
[$encodedSignature, $payload] = $parts;
$signature = base64UrlDecode($encodedSignature);
$decodedPayload = base64UrlDecode($payload);
$data = JSON::decode($decodedPayload);
if (!\is_array($data)) {
throw new SystemException('Payload decoding failed.');
}
if (\strtoupper($data['algorithm'] ?? '') !== 'HMAC-SHA256') {
throw new SystemException('Unknown algorithm. Expected HMAC-SHA256.');
}
$expectedSignature = \hash_hmac('sha256', $payload, $secret, true);
if (!\hash_equals($expectedSignature, $signature)) {
throw new SystemException('Bad Signed JSON signature!');
}
return $data;
}
// Retrieve the signed request from the request parameters
$signedRequest = $_REQUEST['signed_request'] ?? null;
if (!$signedRequest) {
exit;
}
try {
// Parse and verify the signed request
$result = parseSignedRequest($signedRequest, FACEBOOK_PRIVATE_KEY);
} catch (SystemException $e) {
logThrowable($e);
exit;
}
// Extract the user ID from the parsed data
$userID = $result['user_id'] ?? null;
if ($userID && \is_string($userID)) {
$authData = 'facebook:' . $userID;
$user = User::getUserByAuthData($authData);
if ($user->userID > 0) {
try {
// Generate a secure lost password key
$lostPasswordKey = \bin2hex(\random_bytes(20));
// Prepare the data to update the user
$updateData = [
'authData' => '',
'lostPasswordKey' => $lostPasswordKey,
'lastLostPasswordRequestTime' => TIME_NOW,
];
// Execute the update action
(new UserAction([$user], 'update', ['data' => $updateData]))->executeAction();
// Prepare and send the lost password email
$email = new Email();
$email->addRecipient(new UserMailbox($user));
$email->setSubject($user->getLanguage()->getDynamicVariable('wcf.user.lostPassword.mail.subject'));
$email->setBody(new MimePartFacade([
new RecipientAwareTextMimePart('text/html', 'email_lostPassword'),
new RecipientAwareTextMimePart('text/plain', 'email_lostPassword'),
]));
$email->send();
} catch (Throwable $e) {
logThrowable($e);
exit;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment