Invalid Link in the Rainlab User RecoverPassword Mails

Rainlab Users: v3.2.0
OctoberCMS: 3.7.6

I am currently overriding the Rainlab User Components to add some more functionality, but I am a bit confused here.

The task I struggle with is fixing the broken links in the recover password emails, they start with the “?..” instead of a full link.

In order to assure that it’s not my component, I tested the original component but it also had only the url parameters instead of the full link. So… did I do something wrong or is this a bug?

What did I afterwards:


1) Vanilla Theme
First I checked the example theme to see how the original component is used:

The password reset email is send from the authentication component and I see the parameter “resetPage” here. So I checked out the component, but it only has the parameters “rememberMe”, “twoFactorAuth” and “recoverPassword”. “resetPage” does not appear in the Authentiation component nor in the ActionRecoverPassword trait, so I wonder if this is correct or some legacy parameters.

2) Doku
I also checked out the documentation:

It doesnt tell anything about the Authentication component, but I remember testing it and this code didnt work for me:
return $this->account->onLogin();
As far as I remember it told me that onLogin() does not work on null obviously, and I also dont see and I dont see any property called “account” in the Authentication component or the ActionLogin trait, so I wonder if this is up to date as well.

3) Further Research
I had similar problems with the registrationEmail and the activationEmail - I have build a workaround to restore the missing “activate account” functionality.

I found this thread:

I used the sendEmailVerificationNotification in combination with setUrlForEmailVerification for my account activation and similar methods for the registration, but this here will not work for password reset:

        $user->setUrlForPasswordReset(Cms::pageUrl($this->restorePage));
        $user->sendPasswordResetNotification($missingToken);

The problem: sendPasswordResetNotification($token) required a token, ok, so lets check out where the token normally come from and I ended up in Illuminate\Auth\Passwords\PasswordBroker.php / sendResetLink()

…and this is the part where I got compleatly lost…

I also saw that sendPasswordResetNotification() used Cms::entryUrl('resetPassword'), but since I extended the component, this fallback is not useable for me.


Honestly I dont have any idea left what I can do to fix this.

I would like to extend the Authentication component for additional logic and still have the links in the password restore emails to work. No idea how I can fix this, so any help is welcome :slight_smile:

…I found a solution and it is so extremly easy that I had to laugh: I just adjusted the email template in the backend:

# Hello {{ first_name }}

You are receiving this email because we received a password recovery request for your account. If you did not perform this request, you can safely ignore this email.

{% set fullUrl =  config('app.url') ~ '/restore' ~ url %}

{% partial 'button' url=fullUrl body %}
Reset Password
{% endpartial %}

This password reset link will expire in {{ count }} minutes.

{% partial 'subcopy' body %}
If you're having trouble clicking the button, copy the URL below into your browser.

<span class="break-all">{{ fullUrl|raw }}</span>
{% endpartial %}

BUT: Still I think this is a bug in the default password recovery template:

If I reset the email layout AND use the default password recovery, onle the url parameters arrive and NOT the full url: ?reset=d4e34........&email=info@...

For daft: Please have a look if I am wrong here, but I think this needs a fix. ^^

If it needs a fix, you are welcome to submit a PR for review with your suggestion. I’m not sure I can determine what needs fixing from here, easily. The code repo is here: GitHub - rainlab/user-plugin: Front-end user management.

I ran into the same problem. First of all, I have to say that password recovery in the user plugin simply doesn’t work out of the box. The reference installation in the vanilla theme sends the user an email containing only URL parameters (?reset=…&email=…).

The solution of adding the reset URL to the template works, but it’s not elegant—hard-coding the URL in the template is a mess.

When I look at the plugin’s code in Authentication.php, I see that the onRecoveryPassword() callback function calls the recoverPassword() action, which has two parameters: $input is an array from the form (POST), and the second parameter is the $options array. That should contain resetUrl. The plugin authors tested this nicely in their test

ActionManager::instance()->recoverPassword([
    ‘email’ => ‘headless@example.tld’,
], [
    ‘resetUrl’ => ‘https://api.example.tld/reset-password’,
]);

but in the production code, they didn’t use $options; in the onRecoverPassword() callback, there is:

$this->actions()->recoverPassword(post());

The test shows that the code would work when implemented correctly. The concept with ActionManager shows that everything was developed for some headless system where we can add options in the code, but was not adapted for use in user forms.

Honestly, I don’t understand why resetUrl is defined as some obscure option rather than as a hidden field in the reset form. Since I don’t feel like rewriting onRecoverPassword() in Authentication.php, I’ll stick with the solution of adding resetUrl to the template. I hope that in a few years I’ll still remember where that darn resetUrl came from.

I think the docs need to explain this better…

You shouldn’t need resetUrl. The link in the email points at whichever CMS page has the resetPassword component. If several pages have it, the one marked isDefault = 1 wins. If none do, there’s nothing to link to and you get just ?reset=…&email=…, which sounds like what you’re seeing.

So the fix goes on the page, not in the mail template:

title = "Reset Password"
url = "/reset-password"

[resetPassword]
isDefault = 1
==
{% component 'resetPassword' %}

Two catches:

  • The component has to be declared on the page itself. If it’s attached to the layout, or the reset form is a partial on the sign-in page, the plugin won’t find it.
  • If another page also uses resetPassword (we have it on the account security page for changing passwords), mark the reset page isDefault = 1. Otherwise the link may go to the wrong page.

The verification email works the same way with the account component. That’s why the Account component docs show isDefault = 1.

resetUrl is for headless sites, where the reset screen is in a separate app instead of a CMS page. It’s not a form field on purpose, because anyone submitting a form can change its fields. An attacker could request a reset for someone else’s address with resetUrl pointing to their own server. That person would get a genuine reset email from your site, with a working token, linking to the attacker. One click hands over the token, and with it the account.