Laravel Email Verification for Vue SPA
Asked Answered
P

3

9

How can I implement Laravel's Email Verification on a Vue SPA with Vue Router?

So far I have tried to handle email verification by altering the VerificationController verify and resend methods. I then created a new notification and added API routes for the Verification.

When the verification link is generated and sent to user's email, the verification url is something like:

https://foobar.test/email/verify/1?expires=1565276056&signature=b15ccd7d6198bdcf81eea4f5cb441efe8eb2d6d5b57a1ce0b1171e685613d917

When the link is clicked, it opens up a page but it does nothing on the backend as the @verify api route is not hit.

Any suggestions?

VerificationController.php

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Validation\ValidationException;


class VerificationController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Email Verification Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling email verification for any
    | user that recently registered with the application. Emails may also
    | be re-sent if the user didn't receive the original email message.
    |
    */

    use VerifiesEmails;

    /**
     * Where to redirect users after verification.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:600,1')->only('verify', 'resend');
    }

    /**
     * Show the email verification notice.
     *
     */
    public function show()
    {
        //
    }

    /**
     * Mark the authenticated user's email address as verified.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function verify(Request $request)
    {
      $userID = $request[‘id’];
      $user = User::findOrFail($userID);
      $user->email_verified_at = date("Y-m-d g:i:s");
      $user->save();

      return response()->json('Email verified!');
    }

    /**
     * Resend the email verification notification.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function resend(Request $request)
    {
        if ($request->user()->hasVerifiedEmail()) {
            return response()->json('The email is already verified.', 422);
        }

        $request->user()->sendEmailVerificationNotification();

        return response()->json('We have e-mailed your verification link!');
    }


}

VerifyEmail.php

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;


use Illuminate\Support\Facades\URL;
use Carbon\Carbon;


use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;


class VerifyEmail extends VerifyEmailBase
{

    /**
     * Get the verification URL for the given notifiable.
     *
     * @param  mixed  $notifiable
     * @return string
     */
    protected function verificationUrl($notifiable)
    {
      return URL::temporarySignedRoute(
      ‘verification.verify’, Carbon::now()->addMinutes(60), [‘id’ => $notifiable->getKey()]
      );

    }
}

Api.php

Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
Petua answered 8/8, 2019 at 14:16 Comment(2)
You might have missed to add implements MustVerifyEmail to that one. If this interface implementation is missing, the mail checking methods won't do anything. Could you post or check your User model?Woodpecker
Why do you think it isn't hit? It matches exactly. Do you have prefix on your API path?Command
C
2

Faced the same issue with my angular SPA. Not sure you still need help but hope my answer will help somebody.

So while laravel UrlGenerator::signedRoute isn't flexible enough (You can subscribe to this idea. Not the same case, but related to this) we have to implement url signature on our own.

In your VerifyEmail class:

    protected function verificationUrl($notifiable)
    {
        // collect and sort url params
        $params = [
            'expires' => Carbon::now()
                ->addMinutes(Config::get('auth.verification.expire', 60))
                ->getTimestamp(),
            'id' => $notifiable->getKey(),
            'hash' => sha1($notifiable->getEmailForVerification()),
        ];
        ksort($params);

        // then create API url for verification. my API have `/api` prefix,
        // so i don't want to show that url to users 
        $url = URL::route(
            'api:auth:verify',
            $params,
            true
        );

        // get APP_KEY from config and create signature
        $key = config('app.key');
        $signature = hash_hmac('sha256', $url, $key);

        // generate url for yous SPA page to send it to user
        return url('verify-email') . '?' . http_build_query($params + compact('signature'), false);
    }

After that in your SPA you should get url params and invoke API request. I'll specify Angular example but it should be easy to adapt it to Vue.

// on component load
ngOnInit() {

  // get query params from current route   
  this.route.queryParamMap.subscribe(params => {

    // generate API url. Make sure your query params come in the same order
    // as in signature generation. By default signature check middleware 
    // extracts `signature` param so `expires` is the only param that
    // is checked so order doesn't matter, but if you need another params -
    // it can turn into a problem 
    const url = this.router.createUrlTree(['api', 'auth', 'verify', data.id, data.hash],
      {queryParams: {expires: data.expires, signature: data.signature}}).toString();

    // make API request. if signature check fails - you will receive 403 error
    return this.http.get(url).subscribe();
  });
}

Another easier way i see is to generate direct API url and send it to user as you did. And after verifying just redirect browser to your SPA. I just can't understand why it don't work in your case. Maybe you have some rewrite rules in your webserver config so your actual domain name doesn't match with your APP_URL? Or maybe you serve your API in another port?

Command answered 13/11, 2019 at 23:16 Comment(0)
S
1

Here what I did to solve the problem. Go to AuthServiceProvider

/**
   * Register any authentication / authorization services.
   *
   * @return void
   */
  public function boot()
  {
    $this->registerPolicies();

    //

    VerifyEmail::createUrlUsing(function ($notifiable) {
      $params = [
        "expires" => Carbon::now()
          ->addMinutes(60)
          ->getTimestamp(),
        "id" => $notifiable->getKey(),
        "hash" => sha1($notifiable->getEmailForVerification()),
      ];

      ksort($params);

      // then create API url for verification. my API have `/api` prefix,
      // so I don't want to show that url to users
      $url = \URL::route("verification.verify", $params, true);

      // get APP_KEY from config and create signature
      $key = config("app.key");
      $signature = hash_hmac("sha256", $url, $key);

      // generate url for yous SPA page to send it to user
      return env("APP_FRONT") .
        "/auth/verify-email/" .
        $params["id"] .
        "/" .
        $params["hash"] .
        "?expires=" .
        $params["expires"] .
        "&signature=" .
        $signature;
    });
  }
}

add this to api.php

Route::get("/verify-email/{id}/{hash}",
    VerifyEmailController::class)
    ->name("verification.verify");

add this to VerifyEmailController.php

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

 /**
   * Mark the authenticated user's email address as verified.
   *
   * @param  \Illuminate\Foundation\Auth\EmailVerificationRequest  $request
   * @return \Illuminate\Http\RedirectResponse
   */
  public function __invoke(EmailVerificationRequest $request)
  {
    if ($request->user()->hasVerifiedEmail()) {
      return response()->json(
        [
          "message" => "Given email is already verified.",
        ],
        400
      );
    }

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

    return response()->json(
      [
        "message" => "Verification complete.",
      ]
    );
  }
}

Front end

async verfyEmail() {
      try {
        const params = new URLSearchParams(this.$route.query)
        let res = await this.$axios.get(
          'verify-email/' +
            this.$route.params.id +
            '/' +
            this.$route.params.hash,
          { params }
        )
        this.$router.push({ name: 'platform-dashboard' })
      } catch (error) {
        console.log(error.response)
        this.$router.push({ name: 'platform-dashboard' })
      }
    }
Sudor answered 13/11, 2021 at 12:37 Comment(0)
S
0

The solution for this is pretty straightforward. Using the temporarySignedRoute you need to specify the route, the default is verification.verify, expiration time and parameters.

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;


use Illuminate\Support\Facades\URL;
use Carbon\Carbon;


use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;


class VerifyEmail extends VerifyEmailBase
{

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

    }
}
Street answered 11/6, 2020 at 9:28 Comment(1)
This creates the URL that will be in the e-mail the registered user receives but doesn't explain how to handle that in the VueJS frontend. As I understand from the previous answer, the params from that URL need to be handled in a controller but the specifics are not clear as the example uses Angular.Tonettetoney

© 2022 - 2024 — McMap. All rights reserved.