cooperino's avatar

Verification email - Login and verify together

Is it possible to make the verification link both log the user in and then verify? Because right now when I create users, it sends them verification links, then they login, then it tells them to click the verification link again.

I understand that this flow is good when it's for registered users, where it both redirects them to the "verify email" page, but also sends them the link and it's like they're already logged in.

But in my case, an admin adds users and they get the link, but then they must log in, and only then verification link works.

What are my options to make the verification link work as soon as they click it?

0 likes
19 replies
Nakov's avatar

If an admin is the one registering the users and you are sending them invite link, that means that the admin already creates a User record, right? Or is it just an invitation for them to register?

If it is the first option, then when admin is creating the user you can after that call $user->markEmailAsVerified(); or just use

User::forceCreate([ // forceCreate to ignore the $fillable fields, otherwise the `email_verified_at` won't get the value
	.... other fields here
    'email_verified_at' => $this->freshTimestamp(),
])

along with the other fields. Or when you invite them, add a parameter to your url like an invitation_code or whatever, and when they register you again mark them as verified.

Those are couple of options that I can think of. Hope it helps.

1 like
cooperino's avatar

@Nakov For the first option, this is perfect, but for some users I send only the verification link then they register if they want.

What I tried to do was to override the VerifiesEmails trait functions inside the VerificationController, but it doesn't seem to have any effect:

For example, if a user is not registered yet, show him the registration page, for that I tried to override the show() method:

// VerificationController

public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }
}

public function show(Request $request)
    {
        $user = User::find($request->route('id'));
        if(!isset($user->account_id) {
             // verify email and redirect to registration page
        }
        return $request->user()->hasVerifiedEmail()
            ? redirect($this->redirectPath())
            : view('auth.verify');
    }
}

But even if I just do something like

public function show(Request $request)
{
     dd("I'm inside the overriden show");
}

it never goes inside

Nakov's avatar

@cooperino You cannot override them like that, because the routes when they are registered are using the underlying controller not your own. If you go down that path, just create your own middleware and everything, don't just mess things up with what the framework provides.. That's a journey I wouldn't want to be a part of. Good luck.

cooperino's avatar

@Nakov Alright, I thought I can override certain parts, didn't know it's not a good idea.

So assuming I do not want to create my own middleware and everything, and assuming my user now gets a verification email with the structure /email/verify/<id>/<hash>?expires=<date>&signature=<signature>, how to check whether this is a user created by an admin and then verify him instead of redirecting to /login?

Nakov's avatar

@cooperino you can add something more to the url /email/verify/<id>/<hash>?expires=<date>&signature=<signature> that will tell you once the user clicks. Or when creating the user just add an additional column in the users table, created_by_admin and set that one to true or something.

1 like
cooperino's avatar

@Nakov That's what I was initially trying to do, so I copied Laravel's verification stub, and this is the part where I build the URL:

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

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

now when I call this class, I can use the created User object in the constructor to pass it, and add its data to the URL:

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

Now the user_id should be part of the url, right? But where exactly do I check on this url? Because right now, whenever a user clicks the /email/verify URL, it immediately sends him to /login, so I don't know where I should check that?

Snapey's avatar

remove the verification route from the auth middleware

1 like
cooperino's avatar

@Snapey You mean what's inside app\Middleware\Authentican.php? It only has:

    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('login');
        }
    }

Can you please check my reply above and see if it's good idea?

**Edit: as @nakov said, it's a bad idea!

Nakov's avatar

@cooperino I didn't said that, and that's not what @snapey means. There is a verified middleware applied to your routes, you should remove that as he suggests.

1 like
cooperino's avatar

@Nakov got it, it's all the routes that are enclosed in Route::group(['middleware' => ['auth:sanctum', ...., right? But the problem is I can't remove it, I need it. And also, the /email/verify route is not even there, so how does it redirect to /login?

I can't remove this middleware completely, I just want that if a non verified user was created by an admin, then if he clicks the verification button which goes to /email/verify endpoint, then don't immediately redirect to /login, but first do a check and then decide if to redirect, or just verify and login(or send to registration page) immediately

Nakov's avatar

@cooperino Okay, we can go on and on like this forever. I'm done answering to any of your questions. Maybe in couple of months after you've watched some videos, checked the documentation learned at least the basic things about programming and how to get around, then I might respond again. Until then, really good luck.

1 like
Snapey's avatar

@cooperino just create your own verification controller and do whatever you need. You want users to verify their email without being logged in so the route needs to be outside the auth middleware group.

1 like
cooperino's avatar

@Snapey Ok so I did a few things: First I created a custom notification for an added user, and built a special URL:

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

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

But even if I create my own controller, the route is /email/verify/<some_long_string>, and currently, I don't seem to have /email/verify anywhere inside the auth middleware yet it still redirects to /login, so where exactly is it being done? Is it something internally by Laravel, and I'm going to have to also change /email/verify to email/verify-custom for example, and then in web.php:

Route::get('/verify-custom', 'CustomVerificationController@verify_and_login')?

Snapey's avatar

@cooperino In your routes file, routes that are protected are probably inside a route group that applies the auth middleware.

Your verify route should be outside that.

1 like
cooperino's avatar

@Snapey Understood. But I'm confused because currently, not only the /email/verify is not inside this group, but it's not in web.php at all. So I believe it's done by Fortify or somewhere else in vendor.

So what can I do if it is being checked somewhere else by Laravel? I guess I should not have my own email/verify/{id}/{hash} route in web.php in order to not interfere with what's already using it, but instead give it a custom route? email/custom-verify/{id}/{hash}/{user_id} for example?

and change it when building the URL:

        return URL::temporarySignedRoute(
            'verification.custom-verify',
cooperino's avatar

@Snapey I have oauth/cryptkey/passport exception, need to figure out why. For now what I tried was to build a url verification.custom-verify which throws an error that it does not exist. So I assume I can't create my own routes on top of Laravel's built-in verification route (I believe the verification route will show up in artisan route:list after I fix the other issue)

But, if I create my own route without the prefix verification, I'm going to have to manually take care of verification myself from scratch, as seen here: https://laravel.com/docs/8.x/verification ?

Edit: My mistake, verification.verify is the handling route, not the actual link sent.

Edit 2:

created my custom route:

Route::get('/email/verify/{id}/{hash}/{user_id}', function (EmailVerificationRequest $request) {
   //I want to login the user before fulfilling the request, so I don't get null keys, but how do I do it?
    $request->fulfill();

})->name('verification.user-add-verify');

What I need is to create a custom controller:

Route::get('/email/verify/{id}/{hash}/{user_id}','Verify\CustomVerifyController@login_and_verify')
    //  Can I pass the $request to controller?
})->name('verification.user-add-verify');

Please or to participate in this conversation.