BarryJames

BarryJames

Member Since 1 Year Ago

Experience Points
13,490
Total
Experience

1,510 experience to go until the next level!

In case you were wondering, you earn Laracasts experience when you:

  • Complete a lesson — 100pts
  • Create a forum thread — 50pts
  • Reply to a thread — 10pts
  • Leave a reply that is liked — 50pts
  • Receive a "Best Reply" award — 500pts
Lessons Completed
128
Lessons
Completed
Best Reply Awards
0
Best Reply
Awards
  • start your engines Created with Sketch.

    Start Your Engines

    Earned once you have completed your first Laracasts lesson.

  • first-thousand Created with Sketch.

    First Thousand

    Earned once you have earned your first 1000 experience points.

  • 1-year Created with Sketch.

    One Year Member

    Earned when you have been with Laracasts for 1 year.

  • 2-years Created with Sketch.

    Two Year Member

    Earned when you have been with Laracasts for 2 years.

  • 3-years Created with Sketch.

    Three Year Member

    Earned when you have been with Laracasts for 3 years.

  • 4-years Created with Sketch.

    Four Year Member

    Earned when you have been with Laracasts for 4 years.

  • 5-years Created with Sketch.

    Five Year Member

    Earned when you have been with Laracasts for 5 years.

  • school-in-session Created with Sketch.

    School In Session

    Earned when at least one Laracasts series has been fully completed.

  • welcome-newcomer Created with Sketch.

    Welcome To The Community

    Earned after your first post on the Laracasts forum.

  • full-time-student Created with Sketch.

    Full Time Learner

    Earned once 100 Laracasts lessons have been completed.

  • pay-it-forward Created with Sketch.

    Pay It Forward

    Earned once you receive your first "Best Reply" award on the Laracasts forum.

  • subscriber Created with Sketch.

    Subscriber

    Earned if you are a paying Laracasts subscriber.

  • lifer Created with Sketch.

    Lifer

    Earned if you have a lifetime subscription to Laracasts.

  • evangelist Created with Sketch.

    Laracasts Evangelist

    Earned if you share a link to Laracasts on social media. Please email [email protected] with your username and post URL to be awarded this badge.

  • chatty-cathy Created with Sketch.

    Chatty Cathy

    Earned once you have achieved 500 forum replies.

  • lara-veteran Created with Sketch.

    Laracasts Veteran

    Earned once your experience points passes 100,000.

  • 10k-strong Created with Sketch.

    Ten Thousand Strong

    Earned once your experience points hits 10,000.

  • lara-master Created with Sketch.

    Laracasts Master

    Earned once 1000 Laracasts lessons have been completed.

  • laracasts-tutor Created with Sketch.

    Laracasts Tutor

    Earned once your "Best Reply" award count is 100 or more.

  • laracasts-sensei Created with Sketch.

    Laracasts Sensei

    Earned once your experience points passes 1 million.

  • top-50 Created with Sketch.

    Top 50

    Earned once your experience points ranks in the top 50 of all Laracasts users.

Level 3
13,490 XP
Jan
09
4 months ago
Activity icon

Started a new Conversation Laravel API Using Modified 'MustVerifyApiEmail' And Custom 'EnsureApiEmailIsVerified' Middleware Can't Pick Up Request User

I have using Laravel 6.x as a backend with an external (different domain) Vue frontend and do not have register user functionality. The way I register users is by importing batches of users using the Maatwebsite/Laravel-Excel package - which works great.

So when each user is created a job is created by sending each user an email verification link, which when they login for the first time they will need to change their password and simultaneously their email gets marked as verified - which also should work fine.

The problem is that with the already created factory of users, who have their email_verified_at field filled - and the newly imported users - I cannot login as the custom EnsureEmailApiIsVerified middleware does not have access to the $request->user(). I figured out that I can specify the auth guard of 'api' such as $request->user('api') which then can pick up the user, but only of their Bearer token (using Laravel Passport) is sent with the request.

This does make sense as how else would the system know who the request user is without some identifier such as the token. But then how does the standard 'implements MustVerifyEmail' on the User model and subsequent standard 'EnsureEmailIsVerified' middelware on the web routes pick up the $request->user()?

It would stand to reason that either both (standard and my custom) middleware should have access to the $request->user() or both should not.

Now I have had to modify and bring out quite a few framework controllers into my App\Http directory but I have copied them almost verbatim just changing a few things to ensure it works with my API routes - because setting the default guard to 'api' instead of 'web' in config/auth.php had not used it as a default throughout my controllers as thought to be.

So here are the steps I followed:

  1. Created a custom middleware and attached it to the entire 'api' middleware group in App\Http\Kernel.php
/**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
            'verifiedapi',
           
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'role' => \App\Http\Middleware\HasRole::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'verifiedapi' => \App\Http\Middleware\EnsureApiEmailIsVerified::class,
    ];

Then here is that custom 'EnsureApiEmailIsVerified' middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;

class EnsureApiEmailIsVerified
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (! $request->user('api') ||
            ($request->user('api') instanceof MustVerifyApiEmail &&
            ! $request->user('api')->hasVerifiedEmail())) {
            return abort(403, 'Your email has not yet been verified.');
        }

        return $next($request);
    }
}

You will see that references an instance of my custom 'MustVerifyApiEmail' which is a trait that is used on the User Model, with the only diversion from the standard trait being the public function 'sendApiEmailVerificationNotification' as such:

<?php

namespace App\Http\Controllers\API\Auth;

use App\Notifications\VerifyApiEmail;

trait MustVerifyApiEmail
{
    /**
     * Determine if the user has verified their email address.
     *
     * @return bool
     */
    public function hasVerifiedEmail()
    {
        return ! is_null($this->email_verified_at);
    }

    /**
     * Mark the given user's email as verified.
     *
     * @return bool
     */
    public function markEmailAsVerified()
    {
        return $this->forceFill([
            'email_verified_at' => $this->freshTimestamp(),
        ])->save();
    }

    /**
     * Send the email verification notification.
     *
     * @return void
     */
    public function sendApiEmailVerificationNotification()
    {
        $this->notify(new VerifyApiEmail);
    }

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

This new 'sendApiEmailVerificationNotification()' notifies the $request-user('api') with a custom VerifyApiEmail Notification, as such:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Config;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class VerifyApiEmail implements ShouldQueue
{
    use Queueable;

    /**
     * The callback that should be used to build the mail message.
     *
     * @var \Closure|null
     */
    public static $toMailCallback;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Build the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        $verificationUrl = $this->verificationUrl($notifiable);

        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
        }

        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'), $verificationUrl)
            ->line(Lang::get('If you did not create an account, no further action is required.'));
    }

    /**
     * Get the verification URL for the given notifiable.
     *
     * @param  mixed  $notifiable
     * @return string
     */
    protected function verificationUrl($notifiable)
    {
        return URL::temporarySignedRoute(
            'verification.api.verify',
            Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
            [
                'id' => $notifiable->getKey(),
                'hash' => sha1($notifiable->getEmailForVerification()),
            ]
        );
    }

    /**
     * Set a callback that should be used when building the notification mail message.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function toMailUsing($callback)
    {
        static::$toMailCallback = $callback;
    }
}

The new api routes are as follows:

Route::namespace('API\Auth')->group(function () {
    Route::post('login', '[email protected]');
    Route::post('refresh', '[email protected]');
    Route::post('logout', '[email protected]');
    Route::get('email/verify/{id}/{hash}', '[email protected]')->name('verification.api.verify');
    Route::get('email/resend', '[email protected]')->name('api.verification.resend');
});

And the VerificationApiController is as follows:

<?php

namespace App\Http\Controllers\API\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Access\AuthorizationException;

class VerificationApiController extends Controller
{

    /**
     * Show the email verification notice.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function show(Request $request)
    {
        //
    }

    /**
     * Mark the authenticated user's email address as verified.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     *
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function verify(Request $request)
    {
        if (! hash_equals((string) $request->route('id'), (string) $request->user('api')->getKey())) {
            throw new AuthorizationException;
        }

        if (! hash_equals((string) $request->route('hash'), sha1($request->user('api')->getEmailForVerification()))) {
            throw new AuthorizationException;
        }

        if ($request->user('api')->hasVerifiedEmail()) {
            return response()->json(['error' => 'Email already verified'], 422);
        }

        if ($request->user('api')->markEmailAsVerified()) {
            event(new Verified($request->user('api')));
        }

        return response()->json(['success' => 'Email verified!']);
    }

    /**
     * Resend the email verification notification.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function resend(Request $request)
    {
        $request->user('api')->sendApiEmailVerificationNotification();

        return response()->json(['success' => 'Email verification has been resent!']);
    }
}

I also noticed that onn the User Model it extends User as Authenticatable which then uses the standard MustVerifyEmail' - so I brought that out of the framework as well and changed the usage to the new MustVerifyApiEmail - like so:

<?php

namespace App\Http\Controllers\API\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword, MustVerifyApiEmail;
}

My User model then looks like this at the top:

<?php

namespace App;

use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use App\Http\Controllers\API\Auth\User as Authenticatable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmailInterface;

class User extends Authenticatable implements MustVerifyApiEmailInterface
{
    use HasApiTokens, Notifiable;
...

As you can see it's quite a bit of customization - but it should all work in theory and I am getting no errors that I can use. Here are the errors that I get:

When I login with a user who's email is verified or even not verified, I get the error that the user's email has not been verified - but only because it doesn't pick up the $request->user('api'). When I try to throw an error in the middleware itself before returning the request dumping the $request->user('api') it gives me null

So my question is, with the standard middleware of 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - how does this pick up the $request->user()?

Is there something I am missing or am I going about this the wrong way? It seems that when the user logs in it doesn't log them in and then run the middleware - so there is no $request->user('api') - maybe because I am using Passport, but I would think that what should happen is that the middleware needs to run after it has authenticated the user then it would have access to the $request->user('api')

ANY GUIDANCE WOULD BE EXTREMELY APPRECIATED!