I'm using Laravel 7.3 with the built-in user email verification flow.
I'm trying to implement a 'return URL' into the flow, so that I can redirect a user to a specific URL after verifying their email. This is useful for when a user starts an action on the app such as subscribing. That action requires an account, and an account requires verification.
I have included the return URL within the URL at every step of the flow and I'm generating a verification URL that looks like the following:
https://localhost/email/verify/287/23e499466a6bad538df7ce48e090294502502a86?expires=1630005974&signature=f77d57494132c71735e744a224e5116974fec33b482ec5874c40ca6321d200e6&return=https://localhost/subscribe
So far so good. However, this returns a 403. Tracking it down leads me to:
vendor\laravel\framework\src\Illuminate\Routing\UrlGenerator.php
/
Illuminate\Routing\UrlGenerator
public function hasCorrectSignature(Request $request, $absolute = true)
{
$url = $absolute ? $request->url() : '/'.$request->path();
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
return hash_equals($signature, (string) $request->query('signature', ''));
}
Dumping out $original
shows me this: https://localhost/email/verify/287/23e499466a6bad538df7ce48e090294502502a86?expires=1630005974&return=https%3A%2F%2Flocalhost%2Fsubscribe%2F2
It's what I'm expecting, but the function returns false
, which throws the 403.
I can't imagine this is an unusual use-case, but I'm struggling to find anything online about the best way to implement it.
Below is how I'm constructing the URL, this is inside my VerifyEmail
notification. I've tried adding the return
as a parameter of the signed route itself but it also 403s:
$url = URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
// Add this here rather than as a parameter so it doesn't form part of the hash
$url = $url.'&return='.request()->get('return') ?? '';
return $url;
To make things more puzzling, if return
is empty in the verification URL then if verifies just fine. For example this works:
https://localhost.com/email/verify/287/23e499466a6bad538df7ce48e090294502502a86?expires=1630005974&signature=f77d57494132c71735e744a224e5116974fec33b482ec5874c40ca6321d200e6&return=
EDIT:
After some further investigating I've worked out that adding the return URL to the signed URL generation means it works so long as return
is not empty. If an empty string is passed then a 403 is generated when verifying.
$url = URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
'return' => rtrim(request()->get('return'), '/') ?? '',
]
);
return $url;
The above works fine if there is a return
value, otherwise it 403s.