1084

Members module: issue with hotmail prefetching reset password confirmation links

Comments for “Members module: issue with hotmail prefetching reset password confirmation...”
 

Posted by mjim on Wednesday 21st February 2024 at 04:33 GMT

I had two Hotmail users tell me today that they keep getting the ouch page (Either your confirmation link has expired or you have visited a URL that's not valid) when they reset their password.

I signed up for a new Hotmail account (ugh) and ran into the same issue when testing.

Since Hotmail will fetch your page and preview it or check for spam, I suspected it was "clicking" on the link which made it invalid.

I ran a few more tests and it started working so I suspected it was cached (or verified) and wasn't prefetching the link anymore.

To prove my theory, I set up a pr_clicks column at the end of my table. Then in the Account.php file, in init_reset_password(), I added this line immediately after setting the password to empty:

$data['password'] = '';
$data['pr_clicks'] = $member_obj->pr_clicks + 1;


I used a different app so the link wouldn't be cached and after opening the email message in Hotmail (and not clicking the link yet), sure enough it registered as a click:

num_logins: 1
last_login: 1708488881
user_token: 
pr_clicks: 1


I'm considering a workaround where I send only Hotmail users to another page where they enter a 6-digit number so it doesn't matter if the link is prefetched.

Any better ideas?
Level One Member

mjim

User Level: Level One Member

Date Joined: 6/01/2022

Posted by sasin91 on Wednesday 21st February 2024 at 06:45 GMT

Ah yes, providers sometimes “scan” mails and follow links & redirects. Some mail clients and antivirus do so too.

In the past I’ve gotten around Hotmail, GMail and the like by checking the user agent header and skipping app logic and returning http 204.

$userAgent = $_SERVER['HTTP_USER_AGENT'];
$pattern = '/(gmail|msoffice|outlook|bot|spider|crawler)/i';

if (preg_match($pattern, $userAgent) || array_key_exists(‘X-Antivirus’, $_SERVER)) {
    http_status_code(204);
    die(“skipping email verification”);
}

///… rest of your code


You may want to add
I added bot,crawler,spider at the end & checking for typical antivirus header as a little bit of a fallback safety net.

A bit outside scope of this answer, but worthwhile mentioning..
In addition you could also perform a reverse dns lookup
$hostname = gethostbyaddr($ipAddress);
//…

and check if the tld of the hostname belongs to a provider.
If you decide this is necessary too, be aware that, if you’re hosting your app with a reverse proxy in front, kubernetes for instance. Then the request ip will likely be the internal IP, but the real IP may be passed using the X-Forwarded-For header.

This comment was edited by sasin91 on Wednesday 21st February 2024 at 06:51 GMT

sasin91

User Level: Guest

Date Joined: 13/09/2022

Posted by mjim on Wednesday 21st February 2024 at 13:23 GMT

Hey sasin91

I checked the access logs and added the user-agent microsoftpreview to my pattern.

I'd prefer a solution where I don't have to update the pattern every time I find a new email provider issue but in the meantime, this is a good alternative!

Thank you!
Level One Member

mjim

User Level: Level One Member

Date Joined: 6/01/2022

Posted by sasin91 on Wednesday 21st February 2024 at 15:45 GMT

Happy to help 😊

Ya i fully understand that, it’s annoying. Luckily doesn’t change that often.

Another solution could be to add a “enter code from mail” prompt along with the token that would add a bit more protection and prevent crawlers & bots. :)

sasin91

User Level: Guest

Date Joined: 13/09/2022

Posted by mjim on Sunday 25th February 2024 at 16:35 GMT

Here is a very simple fix that works 100% of the time for me.

I still have users running into issues due to other providers such as Outlook not sending any user-agent in the headers.

Since the problem is with prefetching a preview of the link within the email message, I use a landing page for that link instead.

The email message in _init_password_reset sends the user to confirm_reset_password first:
function _init_password_reset($member_obj) {
        $this->module('members');
        $data['user_token'] = make_rand_str(32);
        $this->model->update($member_obj->id, $data, 'members');
        // $reset_url = BASE_URL.'members-account/init_reset_password/'.$data['user_token']; // old URL
        $reset_url = BASE_URL.'members-account/confirm_reset_password/'.$data['user_token'];
        $this->members->_send_password_reset_email($member_obj, $reset_url);
        redirect('members/check_your_email/reset');
    }


The landing page method in Account.php captures the code in segment(3):
// for confirm_reset_password landing page
function confirm_reset_password() {
      $data['reset_code'] = segment(3);
      $data['view_module'] = 'members/account';
      $data['view_file'] = 'confirm_reset_password';
      $this->template('public', $data);
    }


The user simply has to click the link on that page to go to members-account/init_reset_password.

Here is my confirm_reset_password view file:
<section class="container">
    <h2 style="text-align: center">Confirm Password Reset Code</h2>
    <p>This password reset code can only be used once. If you see an error page after submitting the code, use the <a href="<?= BASE_URL ?>members-account/forgot_password">Forgot Password page</a> to resend another code.</p>
    <p style="text-align: center"><?= anchor('members-account/init_reset_password/'.$reset_code, 'Submit Confirm Code', array("class" => "button btn-primary")) ?></p>
</section>


I hope this helps other users who may run into the same issue.

This comment was edited by mjim on Sunday 25th February 2024 at 16:37 GMT

Level One Member

mjim

User Level: Level One Member

Date Joined: 6/01/2022

×