0

I am trying to create a custom verification flow, where as soon as a user clicks the verification link, it logs him in and also verifies him, instead of first making him log in and only then the verification link works.

I built a custom notification URL in my CustomVerificationNotification, including the registered user_id, to login him later:

protected function verificationUrl($notifiable)
{
    if (static::$createUrlCallback) {
        return call_user_func(static::$createUrlCallback, $notifiable);
    }

    return URL::temporarySignedRoute(
        'verification.custom-verify',
        Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
        [
            'id' => $notifiable->getKey(),
            'hash' => sha1($notifiable->getEmailForVerification()),
            'user_id' => $this->user->id
        ]
    );
}

Then in my web.php I added this route:

Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController@login_and_verify')->name('verification.custom-verify');

Then in my CustomVerifyController:

public function login_and_verify(EmailVerificationRequest $request)
{
    //..
}

But I get Call to a member function getKey() on null. And I can't edit EmailVerificationRequest, so what can I do? Is it possible to somehow call Auth::login($user); before calling the EmailVerificationRequest? (Because I have the user_id from the route)

I tried to follow the best answer from this post as well: How to Verify Email Without Asking the User to Login to Laravel

But I'm not sure then how to trigger the verify() method from the web.php and send the $request when I'm first calling the verify_and_login method

pileup
  • 1
  • 2
  • 18
  • 45
  • You don't need to use that `EmailVerificationRequest` request. In fact you could just login the user with `Auth::loginUsingId()` then [verify](https://github.com/laravel/framework/blob/8.x/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php#L49-L52) the user. But, your route **needs** the `signed` middleware. – Clément Baconnier Jan 28 '22 at 10:41
  • You will also need to [validate the hash](https://github.com/laravel/framework/blob/8.x/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php#L22-L25) with the `user_id` given. Probably in a Middleware or a Request – Clément Baconnier Jan 28 '22 at 10:47
  • Thank you. Should I put the `signed` middleware in the constructor of my Controller - `$this->middleware('signed')->only('verify');` (do I need to use `only('verify')` in my case?, or assign it in the route `->middlware['signed']`?. Then, I need to also create my own custom Middleware? Or, how should I do it via Request? I'm having hard time figuring the actual process – pileup Jan 28 '22 at 11:41

1 Answers1

1

First you need verify that the URL is signed by adding the middleware signed

You don't want that anoyone having the url /email/verify/{id}/{hash}/{user_id} able to access this ressource without the signature.

web.php

Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController@login_and_verify')
      ->middleware('signed')
      ->name('verification.custom-verify');

Then you need to verify that the hash correspond the user_id and for that you can use a Request or a Middleware. I think the Request fits better since Laravel already uses a Request for this.

CustomEmailVerificationRequest.php


<?php

namespace App\Http\Requests;

use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Http\FormRequest;

class EmailVerificationRequest extends FormRequest
{

    public function authorize()
    {
       
        $user = User::findOrFail($this->route('id'));


        if (! hash_equals((string) $this->route('hash'), sha1($user->getEmailForVerification()))) {
            return false;
        }

        return true;
    }

}

Finally you need to login with the user and set is email as verified

CustomVerifyController.php


public function login_and_verify(CustomEmailVerificationRequest $request)
{
    $user = User::findOrFail($this->route('id'));
    
    Auth::login($user);
    $user->markEmailAsVerified();

    event(new Verified($user));


    ...

}

[Edit to add addition feature from comments]

In order to have a middleware that verify the signed URL and resend automatically the verification email, you need to build a custom middleware.

ValidateSignatureAndResendEmailVerification.php



namespace App\Http\Middleware;

use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use URL;

class ValidateSignatureAndResendEmailVerification
{

    public function handle($request, Closure $next, $relative = null)
    {
        if(! URL::hasCorrectSignature($request, $relative !== 'relative')( {
            throw new InvalidSignatureException;
        }

        if (URL::signatureHasNotExpired()) {
            return $next($request);
        }


         return redirect()->route('resend-email-confirmation');
    }
}

Then you need to add the middleware to Kernel.php

Kernel.php

    protected $routeMiddleware = [
        ...
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'signed.email' => \App\Http\Middleware\ValidateSignatureAndResendEmailVerification::class,
        ...
    ];

Then, don't forget to update your route with the new middleware

web.php

Route::get('/email/verify/{id}/{hash}/{user_id}','Auth\CustomVerifyController@login_and_verify')
      ->middleware('signed.email')
      ->name('verification.custom-verify');
Clément Baconnier
  • 5,718
  • 5
  • 29
  • 55
  • Perfect. I managed to do it in a slightly different way (That the authorization is done via normal `Request` and all the checks are in the controller) The only thing left to do is to resend the verification email if it's expired. Do you know how do I check whether a signed route is expired? I can't find the correct method – pileup Jan 28 '22 at 17:20
  • 1
    That will be harder. I won't have the time to explain in detail how to do that. From what I see, you need to use a [custom `signed` Middleware](https://github.com/laravel/laravel/blob/8.x/app/Http/Kernel.php#L63) But where it gets complicated; it [check](https://github.com/laravel/framework/blob/8.x/src/Illuminate/Routing/Middleware/ValidateSignature.php#L22) on the Request that the link is valid, but that's actually [a macro](https://github.com/laravel/framework/blob/8.x/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php#L84) – Clément Baconnier Jan 28 '22 at 17:31
  • 1
    So you would need to use the facade [`URL`](https://github.com/laravel/framework/blob/8.x/src/Illuminate/Routing/UrlGenerator.php#L380) in your middleware – Clément Baconnier Jan 28 '22 at 17:33
  • Ok that's more complicated than I thought. I might give it a try though. Instead of explaining in detail since you don't have time, can you please try to explain to me why it's not something as simple as doing `if ($user->routeHasExpired()) { $user->notify(new CustomVerificationNotification); }` inside the controller? – pileup Jan 28 '22 at 20:52
  • I mean `$user->hasValidSignature()` (not `routeHasExpired()`, which I just gave an arbitrary name until I saw there's actually a method called `hasValidSignature()`) – pileup Jan 28 '22 at 20:59
  • Sorry, I mean passing the `$reuqest` from the controller, not `$user->` since it's not a `User` method: `if ($request->hasValidSignature())` from withing the controller, while somehow using the middleware method? Or, using the exception: https://laravel.com/docs/8.x/urls#responding-to-invalid-signed-routes - `InvalidSignatureException` – pileup Jan 28 '22 at 21:09
  • 1
    @TheArchitect I have updated my answer with the custom middleware, it wasn't so hard afterall – Clément Baconnier Jan 31 '22 at 10:57
  • Nice. I did a different thing: I created a separate route with separate route name `custom-verify`. Then created a custom `Notification` that corresponds to it and builds the correct URL (I copied the original email sending `Notification` stub and changed for my needs), and then created a `CustomController` for that route that simply verifies the user that clicks the link he just received with `$user->markEmailAsVerified()`. This controller uses the default `signed` middleware. Is it ok as well? – pileup Feb 01 '22 at 08:19
  • 1
    @TheArchitect If your endpoints as secured with the `sgined` middleware and the hash verification like I have done in **CustomEmailVerificationRequest.php** it should be all right – Clément Baconnier Feb 01 '22 at 09:36