Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

rotaercz's avatar

Email verification using Jetstream

So I have Laravel + Jetstream installed.

I updated my user.php model to implement MustVerifyEmail

I'm currently getting the error:

Symfony\Component\Routing\Exception\RouteNotFoundException
Route [verification.verify] not defined.

I read through the Email Verification documentation and it sounds like most of what needs to be done is for someone that's not using Jetstream. Since I have Jetstream already installed I'm not sure what the right way to set this up would be.

0 likes
31 replies
rodrigo.pedra's avatar

Email verification is a Laravel core feature, not a feature added by JetStream.

I wrote about it in a thread yesterday:

https://laracasts.com/discuss/channels/laravel/laravel-breeze-sending-registration-emails-email-customizations

Regarding the missing routes, check this excerpt from the docs:

To properly implement email verification, three routes will need to be defined.

https://laravel.com/docs/8.x/verification#verification-routing

You should manually the routes needed for this feature as listed in the docs.

2 likes
rotaercz's avatar

Thank you!

I got the email verification up and running and it works great. I still have two questions...

@rodrigo.pedra You helped me setup the API it's been great. I added more functionality where I track the user's deviceID. When the user's deviceID changes, I'd like to send out a verification email as an extra security measure. In my ApiLoginController I have something that looks like this:

if ($user->device_id != $credentials['deviceID']) { // deviceID mismatch, verify email
            $user->email_verified_at = null;
            $user->save();



            // send out verification email here...

            // on email verification save new deviceID



            return response()->json(['message' => 'email verification required'], 403);
}

What code do I have to add here to send out the verification email and save the new deviceID?

Second question, where is the email verification view so I can change the wording to be a bit more general in the email to accommodate both the sign up and the please verify again scenario?

rodrigo.pedra's avatar

where is the email verification view so I can change the wording...

There isn't a view for that notification. The mail message is built using a MailMessage instance.

I talk about that on the other thread I linked in my first response, and show how you could modify it.

For reference, this is the other thread link:

https://laracasts.com/discuss/channels/laravel/laravel-breeze-sending-registration-emails-email-customizations

// send out verification email here...

// on email verification save new deviceID

You could tweak the current email verification code flow to account for device ID.

  1. Add a column to the user's model to save the device id.
  2. Change the VerifyEmail notification (mentioned on the other thread) to send the device id within the confirmation route link.
  3. Change the confirmation code to verify if device id sent in route is the same you last saved on user's model.

If you want to allow multiple devices per user, you might need a separate table to hold authorized device ids.

Hope it helps.

1 like
rotaercz's avatar

I did as you said and made a new class VerifyDeviceIDEmail that overrides VerifyEmail. Using migrations I added two columns to the user table. One for deviceID and another for when it's verified. Similar to how email verification is setup. I was following the example that you explained and I wasn't sure what to edit for updating a different table column (instead of the email verification column). Which part of the code should I be looking at?

rodrigo.pedra's avatar

From the docs it suggests this code to handle email verification:

use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\Request;

Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
    $request->fulfill();

    return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');

https://laravel.com/docs/8.x/verification#the-email-verification-handler

If you look at Illuminate\Foundation\Auth\EmailVerificationRequest class' fullfill() method implementation, it does:

    public function fulfill()
    {
        $this->user()->markEmailAsVerified();

        event(new Verified($this->user()));
    }

So you could add to your User model an overriden implementation of markEmailAsVerified() that updates the device id verification column.

rodrigo.pedra's avatar

Step-by-step

Edit: made some fixes on the custom request class

Step 1

on your verification code save the new device id, reset the email_verified_at and device_verified_at columns (guesses this column name) and save the current device id:

 // deviceID mismatch, verify email
if ($user->device_id != $credentials['deviceID']) {
    // reset verification values
    $user->email_verified_at = null;
    $user->device_verified_at = null;
    
    // save device id to be validated
    $user->device_id = $credentials['deviceID'];
    
    $user->save();

    $user->notify(new VerifyDeviceIDEmail());

    return response()->json(['message' => 'email verification required'], 403);
}

Step 2

On VerifyDeviceIDEmail notification add the device ID to your signed URL

<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\URL;

//////////////////////////////////////////////////
// extend framework's VerifyEmail Notification
//////////////////////////////////////////////////
class VerifyDeviceIDEmail extends VerifyEmail
{
    protected function buildMailMessage($url)
    {
        //////////////////////////////////////////////////
        // Customize notification message
        //////////////////////////////////////////////////
        return (new MailMessage())
            ->subject(Lang::get('Verify Email Address'))
            ->line(Lang::get('Please click the button below to verify your email address.'))
            ->action(Lang::get('Verify Email Address'), $url)
            ->line(Lang::get('If you did not create an account, no further action is required.'));
    }

    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()),

                //////////////////////////////////////////////////
                // Add device hash to signed URL
                //////////////////////////////////////////////////
                'device' => sha1($notifiable->device_id),
            ]
        );
    }
}

Step 3

Create a new FormRequest to validate this new signed URL. We will base it on the Illuminate\Foundation\Auth\EmailVerificationRequest implementation

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Auth\EmailVerificationRequest;

//////////////////////////////////////////////////
// extend framework's EmailVerificationRequest Notification
//////////////////////////////////////////////////
class DeviceAndEmailVerificationRequest extends EmailVerificationRequest
{
    public function authorize()
    {
        //////////////////////////////////////////////////
        // parent class already checks user id and hash
        //////////////////////////////////////////////////
        if (! parent::authorize()) {
            return false;
        }

        //////////////////////////////////////////////////
        // email only validation  - ADDED
        //////////////////////////////////////////////////
        if ($this->query->has('device')) {
            return true;
        }

        //////////////////////////////////////////////////
        // check device id hash  - CHANGED
        //////////////////////////////////////////////////
        if (! hash_equals((string) $this->query('device'), sha1($this->user()->device_id))) {
            return false;
        }

        return true;
    }

    public function fulfill()
    {
        //////////////////////////////////////////////////
        // If query has device id, confirm it - ADDED
        //////////////////////////////////////////////////
        if ($this->query->has('device')) {
            $this->user()->device_verified_at = now();
        }

        //////////////////////////////////////////////////
        // keep parent fullfill business logic
        //////////////////////////////////////////////////
        parent::fulfill();
    }
}

Note we defer to the framework's base class method, to take advantage of future improvements on framework code.

Step 4

In the "The Email Verification Handler" use our new Request class instead of the base class. The code suggested in the docs can keep the same.

https://laravel.com/docs/8.x/verification#the-email-verification-handler

Hope it helps.

1 like
rotaercz's avatar

When I register a new user and try to do an email verification I'm getting 403 THIS ACTION IS UNAUTHORIZED.

Is it really ok to replace the old route to this?

Route::get('/email/verify/{id}/{hash}', function (DeviceAndEmailVerificationRequest $request) {
    $request->fulfill();
    return redirect('/');
})->middleware(['auth', 'signed'])->name('verification.verify');

I'm confused. Doesn't this make it so the normal email confirmation also requires the device_id? The normal email confirmation and the device confirmation system should work independently from each other.

rodrigo.pedra's avatar

It is a valid option having them separated.

But I made some changes on the custom form request that might help you out

rotaercz's avatar

I'm still getting a 403 THIS ACTION IS UNAUTHORIZED for the normal email verification.

I'm thinking it has to be separated because when a user tries to login from a new device it should ask for email verification. But for the old device it should just work as normal. By setting $user->email_verified_at = null; this would also lock out the user that's still on the old device.

Ideally only the new device should require email verification but the old device should be able to authenticate as usual.

1 like
rotaercz's avatar

@rodrigo.pedra

Regarding $this->user()->device_id, in this part of the code:

        //////////////////////////////////////////////////
        // check device id hash  - CHANGED
        //////////////////////////////////////////////////
        if (! hash_equals((string) $this->query('device'), sha1($this->user()->device_id))) {
            return false;
        }

I made a new table and model for user devices so how can I do something like $this->device() instead of $this->user()?

The Device table looks like this:

id, email, device, device_verified_at 

Like even in the fullfil() function there's a $this->user()->device_verified_at = Date::now(); but I have to update the device table instead of the user table.

Basically how can I get the corresponding user row from the device table so I can confirm and update?

rodrigo.pedra's avatar

At first it wasn't clear you would want to have many devices per user.

Considering that, what about adding a user_id column into the devices table?

id, user_id, email, device, device_verified_at 

I don't know if email would still be necessary with this change. If a user changes their email they should still be able to track their connected devices, wouldn't they? If so I would remove the email column from the devices table altogether.

If you make this change then you can add a hasMany relation between the User and Device models.

And then you could do something like this:

$device = $this->user()->devices()->firstWhere('device_id', $this->query('device'));

if (! $device) {
    return false;
}

Does that make sense?

1 like
rotaercz's avatar

Ooooh! That's really clever and makes a lot of sense. I'll go try this out!

rotaercz's avatar

So I applied everything you mentioned and have two more questions!

First question is, in ApiLoginController I'm currently using two queries. Is there a way to reduce it to one query?

Second question, I'm currently not getting a device verification email. Could you take a look and tell me what I'm doing wrong? (Is it ok for the Device model to extend Model instead of Authenticatable like the User model?)

ApiLoginController:

        // check if this user has a verified device
        $device = $user->devices()->firstWhere('user_id', $user['id']); // if this is empty, it's a new user

        if(!$device) { // new user
            $d = new Device();
            $d->user_id = $user['id'];
            $d->device = $credentials['device'];
            $d->device_verified_at = Date::now();
            $d->save();
        }
        else { // existing user
            $device = $user->devices()->firstWhere('device', $credentials['device']); // check if current device exists in table

            if (!$device) { // new device, verify email
                $d = new Device();
                $d->user_id = $user['id'];
                $d->device = $credentials['device']; // save device id to be validated
                $d->device_verified_at = null;
                $d->save();

                $d->sendEmailVerificationNotification();

                return response()->json(['message' => 'device verification required'], 403); // return 403, Unity client will display message informing user to verify email
            }
        }

Device Model:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Notifications\Notifiable;
use App\Http\Controllers\Device\VerifyDeviceEmail;

class Device extends Model implements MustVerifyEmail {
    use HasFactory;
    use Notifiable;
    
    protected $table = 'devices';

    protected $fillable = [
        'email',
        'device',
    ];

    protected $casts = [
        'device_verified_at' => 'datetime',
    ];
   	
    // determine if the user has verified their device (returns bool)
    public function hasVerifiedEmail() {
        return ! is_null($this->device_verified_at);
    }

    // mark the given user's email as verified (returns bool)
    public function markEmailAsVerified() {
        return $this->forceFill([
            'device_verified_at' => $this->freshTimestamp(),
        ])->save();
    }

    // send the email verification notification (returns void)
    public function sendEmailVerificationNotification() {
        $this->notify(new VerifyDeviceEmail);
    }

    // get the email address that should be used for verification (returns string)
    public function getEmailForVerification() {
        return $this->email;
    }
}

VerifyDeviceEmail:

namespace App\Http\Controllers\Device;

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\URL;

class VerifyDeviceEmail extends VerifyEmail {

    protected function buildMailMessage($url) {
        return (new MailMessage())
            ->subject(Lang::get('Verify Device'))
            ->line(Lang::get('Please click the button below to verify your device.'))
            ->action(Lang::get('Verify Device'), $url)
            ->line(Lang::get('If you did not try to login from a new device, ignore this email.'));
    }

    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()),
                'device' => $notifiable->device,
            ]
        );
    }
}

DeviceVerificationRequest:

namespace App\Http\Controllers\Device;

use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Support\Facades\Date;

class DeviceVerificationRequest extends EmailVerificationRequest {

    public function authorize() {
        // parent class already checks user id and hash
        if (! parent::authorize()) {
            return false;
        }

        if (! $this->query->has('device')) { // email only validation (no device to verify)
            return true;
        }
        else { // check if device matches
            $device = $this->user()->devices()->firstWhere('device', $this->query('device'));

            if (! $device) {
                return false;
            }
        }

        return true;
    }
    
    public function fulfill() {
        if ($this->query->has('device')) {
            $device = $this->user()->devices()->firstWhere('device', $this->query('device'));
            $device->device_verified_at = Date::now();
        }

        parent::fulfill();
    }
}
rodrigo.pedra's avatar

Do you still have the email column on your device table?

Asking because on the ApiLoginController I don't see you saving it for new devices.

As you are using the device as the Notifiable instance you'll need one there. Otherwise the notification won't know which email address to use.

If you don't have an email there anymore you can add an accessor to defer to the related user model:

class Device extends Model implements MustVerifyEmail {
    // ... other code

    public function getEmailAttribute() {
        return $this->user->email;
    }
}
rotaercz's avatar

No, I don't have an email column in the device table anymore. I thought the way you suggested was cleaner so I removed the email column from Device. I added the getEmailAttribute() function to the Device model but I'm getting an error Trying to get property 'email' of non-object. I seem to be having trouble with the accessor. How do you access the User from Device?

The columns in the Device model are:

id, user_id, device, device_verified_at, created_at, updated_at.
rotaercz's avatar

The way you suggested didn't seem to work for me for some reason. So I tried querying like this instead:

    public function getEmailAttribute() {
        $user = User::query()->firstWhere('id', $this->user_id);
        return $user->email;
    }

Is it ok to do it this way?

EDIT: Another question, as I was going through the DeviceVerificationRequest code I realized that the parent::authorize() won't work correctly because $this->route('id') is getting the Device table id instead of the User table id. I remember you telling me it's good practice to just extend EmailVerificationRequest so I can get the updates later so I'm trying not to modify it. If I copy paste the parent code to DeviceVerificationRequest and modify it I basically won't be using the parent code though. Do you have any advice on how to handle this scenario?

rodrigo.pedra's avatar

First: The way I suggested didn't work for you because I assume you had a relation set in the Device model to query its related User, if you add the relation it might work:

class Device extends Model implements MustVerifyEmail {
    // ... other code

    public function getEmailAttribute() {
        return $this->user->email;
    }

    public function user() {
        return $this->belongsTo(User::class);
    }
}

Second: I've been thinking about this thread and maybe the approach we have already isn't the best. From the beginning it wasn't to me you would want a user to have multiple authorized devices, that is way I hinted on extending the built-in verify feature to allow verifying devices.

Unfortunately today I will be very busy, so I won't be able to outline a better approach until later this week.

If you can't wait, consider building the device validation routes/middleware without extending the verify email feature. You can base you implementation on it, but just don't rely on extending and modifying as there are a lot of moving parts that assume a email validation and the end result could be messier than an implementation from scratch.

If you can wait, I'll try outlining some solution later this weel.

Thanks

1 like
rotaercz's avatar

Man, thank you!

I'll wait. I can tell you're quite good and know what you're doing. I'd like to see a complete solution. :)

rodrigo.pedra's avatar
Level 56

Step 1 - Scaffold

I scaffolded an app with these commands:

laravel new laracasts --jet
cd laracasts
php artisan make:model Device -m
php artisan make:request VerifyEmailRequest
php artisan make:listener SetDeviceInSession --event=Illuminate\Auth\Events\Login

The frontend stack I chose was Livewire, but the steps done here would work with Inertia as well.

Step 2 - Views

I added the field below on both ./resources/views/auth/login.blade.php and ./resources/views/auth/register.blade.php forms:

<div class="mt-4">
    <x-jet-label for="device" value="{{ __('Device') }}" />
    <x-jet-input id="device" class="block mt-1 w-full" type="text" name="device" :value="old('device')" required />
</div>

Step 3 - Migrations

I didn't change any migrations shipped with Laravel or JetStream.

This is the code for the create_devices_table migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateDevicesTable extends Migration
{
    public function up()
    {
        Schema::create('devices', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();

            $table->string('name');
            $table->timestamp('verified_at')->nullable();

            $table->unique(['user_id', 'name']);

            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('devices');
    }
}

Step 4 - Models

The Device model is pretty simple:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Device extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'name',
        'verified_at',
    ];

    protected $casts = [
        'verified_at' => 'datetime',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

On the User model is where more modifications live on:

<?php

namespace App\Models;

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens;
    use HasFactory;
    use HasProfilePhoto;
    use Notifiable;
    use TwoFactorAuthenticatable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_recovery_codes',
        'two_factor_secret',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = [
        'profile_photo_url',
    ];

    public function devices()
    {
        return $this->hasMany(Device::class);
    }

    public function hasVerifiedEmail()
    {
        if (is_null($this->email_verified_at)) {
            return false;
        }

        if (! \session()->has('device')) {
            return false;
        }

        $device = \session()->get('device');

        return ! \is_null($device->verified_at);
    }

    public function markEmailAsVerified()
    {
        $now = $this->freshTimestamp();

        $device = \session()->get('device');

        if ($device && \is_null($device->verified_at)) {
            $device->forceFill(['verified_at' => $now])->save();
        }

        if (\is_null($this->email_verified_at)) {
            return $this->forceFill(['email_verified_at' => $now])->save();
        }

        return false;
    }

    public function sendEmailVerificationNotification()
    {
        if (\session()->has('device')) {
            $device = \session()->get('device');

            VerifyEmail::$createUrlCallback = function ($user) use ($device) {
                return URL::temporarySignedRoute(
                    'verification.verify',
                    Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
                    [
                        'id' => $user->getKey(),
                        'hash' => sha1($user->getEmailForVerification()),
                        'device' => Crypt::encryptString($device->getKey()),
                    ]
                );
            };
        }

        $this->notify(new VerifyEmail());
    }
}

Notable changes:

  • Added the implements MustVerifyEmail, as per docs
  • Added the devices relation
  • Override hasVerifiedEmail to check if the current device in session is already verified
  • Override markEmailAsVerified to also mark the current device in session as verified
  • Override sendEmailVerificationNotification to add the device id as a encrypted query parameter

Step 5 - Modified JetStream Actions

JetStream creates an Actions folder to allow a developer to customize some actions performed.

I just modified the CreateNewUser action to also create a new Device:

<?php

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    use PasswordValidationRules;

    /**
     * Validate and create a newly registered user.
     *
     * @param  array  $input
     * @return \App\Models\User
     */
    public function create(array $input)
    {
        Validator::make($input, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => $this->passwordRules(),
            'device' => ['required'],
        ])->validate();

        $user = User::create([
            'name' => $input['name'],
            'email' => $input['email'],
            'password' => Hash::make($input['password']),
        ]);

        $device = $user->devices()->create(['name' => $input['device']]);

        \session()->put('device', $device);

        return $user;
    }
}

Step 6 - VerifyEmailRequest

As listed on Step 1 - Scaffold I created a new VerifyEmailRequest form request to extend the one shipped with Laravel.

The idea is to extract the device from the signed URL sent in the verification URL and set it into the session:

<?php

namespace App\Http\Requests;

use Illuminate\Support\Facades\Crypt;

class VerifyEmailRequest extends \Laravel\Fortify\Http\Requests\VerifyEmailRequest
{
    protected function prepareForValidation()
    {
        $device = $this->extractDeviceFromQuery();

        if ($device) {
            $this->session->put('device', $device);
        }
    }

    private function extractDeviceFromQuery()
    {
        $deviceId = $this->query('device');

        if (! $deviceId) {
            return null;
        }

        try {
            $deviceId = Crypt::decryptString($deviceId);
        } catch (\Throwable $exception) {
            return null;
        }

        return $this->user()->devices()->find($deviceId);
    }
}

Step 7 - SetDeviceInSession

I also created a listener for the Illuminate\Auth\Events\Login, to set the current device in session when a user logs in:

<?php

namespace App\Listeners;

use App\Models\Device;
use Illuminate\Auth\Events\Login;

class SetDeviceInSession
{
    public function handle(Login $event)
    {
        $deviceName = \request()->input('device');

        if (! $deviceName) {
            \session()->forget('device');

            return;
        }

        $device = Device::query()->updateOrCreate([
            'user_id' => $event->user->getKey(),
            'name' => $deviceName,
        ]);

        \session()->put('device', $device);

        if (\is_null($device->verified_at)) {
            $event->user->sendEmailVerificationNotification();
        }
    }
}

Step 8 - Wire up

To tell Laravel to use the VerifyEmailRequest form request, I registered it in the FortifyServiceProvider:

<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Requests\VerifyEmailRequest;

class FortifyServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(VerifyEmailRequest::class, \App\Http\Requests\VerifyEmailRequest::class);
    }
    

    public function boot()
    {
        // these were added by JetStream
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
    }
}

To tell Laravel to call the SetDeviceInSession listener, I registered it in the EventServiceProvider:

<?php

namespace App\Providers;

use App\Listeners\SetDeviceInSession;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        // this one was already added by Laravel/JetStream
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        
        // this one was the one I added
        Login::class => [
            SetDeviceInSession::class,
        ],
    ];

    public function boot()
    {
        //
    }
}

Hope this helps.

4 likes
rotaercz's avatar

This is a beautiful answer.

How would you do it in a scenario where there's no sessions? Like through an API controller?

EDIT: Also I'll send you a link to the game when I'm done if you tell me your email address.

rodrigo.pedra's avatar

Without session you could send the device ID as a custom request header.

Not sure how to deal with it using Fortify, as Fortify is meant to be used with a session-based guard, so I assumed a session-available setup.

Reference: https://github.com/laravel/fortify/search?q=StatefulGuard&type=code

But completely doable with small tweaks to the code above.

I don't post my email address in forums to avoid spam, but my twitter account is linked in my Laracasts' profile.

DanVan's avatar

Hi guys. I am trying to implement what the doc 8.x says about email authentication. I think I managed to follow everything, but I am yet not successful.

This is my User.php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Jetstream\HasTeams;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens;
    use HasFactory;
    use HasProfilePhoto;
    use HasTeams;
    use Notifiable;
    use TwoFactorAuthenticatable;

This is part of my Fortify.php


    'features' => [
        Features::registration(),
        Features::resetPasswords(),
        Features::emailVerification(),
        Features::updateProfileInformation(),
        Features::updatePasswords(),
        Features::twoFactorAuthentication([
            'confirmPassword' => true,
        ]),

and this is my web.php containing the three needed routes.

<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
    return view('dashboard');
})->name('dashboard');

// Email verification notice
Route::get('/email/verify', function () {
    return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');

// The Email Verification Handler

Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
    $request->fulfill();

    return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');

// Resending The Verification Email

Route::post('/email/verification-notification', function (Request $request) {
    $request->user()->sendEmailVerificationNotification();

    return back()->with('message', 'Verification link sent!');
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');

I do not quite understand what I am doing wrong. The error I am getting is the following:

Swift_TransportException Connection could not be established with host mailhog :stream_socket_client(): php_network_getaddresses: getaddrinfo failed: nodename nor servname provided, or not known

UPDATE - the problem is with the .env file and the email credentials. However, I cannot fix it.

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
[email protected]
MAIL_PASSWORD=psw
MAIL_ENCRYPTION=ssl
[email protected]
MAIL_FROM_NAME="${APP_NAME}"

And getting a new error:

Failed to authenticate on SMTP server with username "[email protected]" using 3 possible authenticators. Authenticator LOGIN returned Expected response code 235 but got code "535", with message "535-5.7.8 Username and Password not accepted. Learn more at 535 5.7.8 https://support.google.com/mail/?p=BadCredentials s1sm71829343wrv.97 - gsmtp ". Authenticator PLAIN returned Expected response code 235 but got code "535", with message "535-5.7.8

Thank you very much and happy new year!

MarkJC's avatar

@DanVan Hi I don't know if this helps. This "edit'ed mailgun in .env is working for me. I.e. 6 lines only. Best to use them? One can go around and around with setting up mail and problem is often outside your control or error you cannot see due to config demands on the providers side. Best to go with simple below that works? MAIL_DRIVER=mailgun MAILGUN_DOMAIN=blablabla.org MAILGUN_SECRET=key-544b9bdblablablabla348 MAIL_PORT=587 [email protected] MAIL_PASSWORD=0ca25ba0blablablabal-cf6fa5ad

Snapey's avatar

@MarkJC should you be posting your private credentials?

I doubt they have been waiting a year for your reply

MarkJC's avatar

Hi Thank you Rodrigo. Your best answer above worked for me! Awesome!!! I was really trepidacious about using Jetstream but your answer has email verification up and running.

MarkJC's avatar

So I have another question. Not sure anyone can assist me. I started looking at incorporating email verification (best reply above) into YouTube's designatedcoder walkthrough (17 movies) to incorporate spatie dashboard into Laravel 8 + Jetstream. Not sure where I went wrong but I am now getting "Illuminate\Contracts\Container\BindingResolutionException Target class [Auth\VerificationController] does not exist." If anyone has an idea on how to resolve this I would appreciate it. Anything to point me in the right direction. I am punch drunk from this. Been at it for hours. PS I love the potential of designatedcoder's approach re the admin dashboard - which is working but now not with email verification. :~(

Sinnbeck's avatar

@MarkJC Make a new thread.. You can add a link to this one if you want to add some context

Please or to participate in this conversation.