1

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.

Mike
  • 8,767
  • 8
  • 49
  • 103

0 Answers0