19

I am developing a Laravel application. My application is using Laravel built-in auth feature. In the Laravel auth when a user registers, a verification email is sent. When a user verifies the email click on the link inside the email, the user has to login again to confirm the email if the user is not already logged in.

VerificationController

class VerificationController extends Controller
{
    use VerifiesEmails, RedirectsUsersBasedOnRoles;

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

    public function redirectPath()
    {
        return $this->getRedirectTo(Auth::guard()->user());
    }
}

I tried commenting on this line.

$this->middleware('auth');

But it's s not working and instead, throwing an error. How can I enable Laravel to be able to verify email even if the user is not logged in?

Karl Hill
  • 6,972
  • 3
  • 39
  • 64
Wai Yan Hein
  • 9,841
  • 21
  • 103
  • 244
  • Why do you want this? I can just MitM that email and hijack any account now. – Loek Dec 21 '18 at 13:18
  • What does `RedirectsUsersBasedOnRoles` do? When you say ‘It is not working.’ what do you mean; you get an error? What kind of error? – Thomas Edwards Dec 21 '18 at 13:20
  • 1
    It throws an error because it expects an instance of your user. See: https://github.com/laravel/framework/blob/f88917adc292e7e2960e9336a0d89206b41155fe/src/Illuminate/Foundation/Auth/VerifiesEmails.php#L35 – adam Dec 21 '18 at 15:58
  • Thanks adam. Thanks Loek for pointing out with a reasonable reason. Cheers – Wai Yan Hein Dec 21 '18 at 16:20
  • how so @Loek? the url is signed. – AaronHS Aug 15 '19 at 07:12

7 Answers7

31

First, remove the line $this->middleware('auth');, like you did.

Next, copy the verify method from the VerifiesEmails trait to your VerificationController and change it up a bit. The method should look like this:

public function verify(Request $request)
{
    $user = User::find($request->route('id'));

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

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

    return redirect($this->redirectPath())->with('verified', true);
}

This overrides the method in the VerifiesUsers trait and removes the authorization check.

Security (correct me if I'm wrong!)

It's still secure, as the request is signed and verified. Someone could verify another user's email address if they somehow gain access to the verification email, but in 99% of cases this is hardly a risk at all.

flexponsive
  • 5,516
  • 6
  • 23
  • 36
Wouter Florijn
  • 2,181
  • 1
  • 18
  • 35
3
// For Laravel 6 and Above 
use Illuminate\Auth\Events\Verified; 
use Illuminate\Http\Request; 
use App\User;

// comment auth middleware
//$this->middleware('auth');

public function verify(Request $request)
{
    $user = User::find($request->route('id'));

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

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

    return redirect($this->redirectPath())->with('verified', true);
}
Abid Shah
  • 197
  • 1
  • 3
2

Here's a more future proof solution to the problem:

class VerificationController extends Controller
{

    // …

    use VerifiesEmails {
        verify as originalVerify;
    }

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

    /**
     * Mark the authenticated user's email address as verified.
     *
     * @param Request $request
     * @return Response
     *
     * @throws AuthorizationException
     */
    public function verify(Request $request)
    {
        $request->setUserResolver(function () use ($request) {
            return User::findOrFail($request->route('id'));
        });
        return $this->originalVerify($request);
    }
}

So when an email confirmation link is clicked by an unauthenticated user the following will happen:

  1. User will be redirected to the login view 1
  2. User enters credentials; logs in successfully 2
  3. User will be redirect back to the email confirmation URL
  4. Email will be marked as confirmed

1 The email will not be marked as confirmed at this point.

2 The user may enter bad credentials multiple times. As soon as he enters the correct credentials he will be redirected to the intended email confirmation URL.

Martin
  • 531
  • 1
  • 6
  • 18
1

You should not remove $this->middleware('auth') altogether as that will effect the redirects. If you remove it, the unauthenticated users will be redirected to "/email/verify" instead of "/login"

so $this->middleware('auth'); will be changed to $this->middleware('auth')->except('verify'); in "VerificationController"

Also copy the "verify" function from "VerifiesEmails" into "VerificationController"

add these two lines of code at the top of the function

$user = User::find($request->route('id'));

auth()->login($user);

so you are logging in the user programmatically and then performing further actions

Junaid Masood
  • 420
  • 10
  • 18
0

if you want to active user account without login you can do that in 2 steps

1- Remove or comment Auth middleware in VerificationController

Example below:

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

2- since verify route passing the {id} you can just edit verify function to find the user by the route id request like code below :

file path : *:\yourproject\vendor\laravel\framework\src\Illuminate\Foundation\Auth\VerifiesEmails.php

$user = User::findOrfail($request->route('id'));

Complete example

public function verify(Request $request)
{
    $user = User::findOrfail($request->route('id'));

    if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) {
        throw new AuthorizationException;
    }

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

    if ($user->hasVerifiedEmail()) {
        return redirect($this->redirectPath())->with('verified', true);
    }

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

    return redirect($this->redirectPath())->with('registered', true);
}
0

Solution to allow email verification for users who are not logged in (i.e. without auth):

Changes to: app/Http/Controllers/Auth/VerificationController.php:

  1. $this->middleware('auth'); to $this->middleware('auth')->except('verify');
  2. Copy verify() method from the VerifiesEmails trait.
  3. Edit verify method to work without expected $request->user() data.

My verify() method in the VerificationController looks like this:

public function verify(\Illuminate\Http\Request $request)
{
    $user = User::find($request->route('id'));

    if ($request->route('id') != $user->getKey()) {
        throw new AuthorizationException;
    }

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

    return redirect()->route('login')->with('verified', true);
}

Signed middleware

Laravel uses a middleware named signed to check the integrity of URLs that were generated by the application. Signed checks whether the URL has been changed since it was created. Try changing the id, expiry time or the signature in the url and it will lead to an error - very effective and useful middleware to protect the verify() method

For more information: https://laravel.com/docs/8.x/urls#signed-urls

(Optional)

I redirected my users to the login route, rather than the intended route for two reasons. 1) After login, it would try to redirect the user to the email verification link, leading to an error; 2) I wanted to use the verified true flash data that was attached to the redirect, to show an alert on the login page, if the user had successfully verified their email address.

Example of my login page alert:

@if(session()->has('verified'))
    <div class="alert alert-success">Your email address has been successfully verified.</div>
@endif

Suggestions

If you have any suggestions on how I could improve this code, please let me know. I'd be happy to edit this answer.

Darren Murphy
  • 626
  • 6
  • 9
0

Here's my take on the situation. Verification requires user to login before it can complete the verification, so we can override the verify function and login user using ID we received in the link. It is safe cause verify function is not called if Laravel can't verify the signature from URL so even if someone temper the URL they won't be able to bypass it.

Go to your VerificationController and add the following function at the end of the file.

public function verify(Request $request)
{
    if (!auth()->check()) {
        auth()->loginUsingId($request->route('id'));
    }

    if ($request->route('id') != $request->user()->getKey()) {
        throw new AuthorizationException;
    }

    if ($request->user()->hasVerifiedEmail()) {
        return redirect($this->redirectPath());
    }

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

    return redirect($this->redirectPath())->with('verified', true);
}

Note

Make sure you have same_site value in 'config/session.php' set to 'lax'. If it is set to 'strict' then it won't persist session if you were redirected from another site. For example, if you click a verification link from Gmail then your session cookie won't persist, so it won't redirect you to dashboard, but it sets 'email_verified_at' field in the database marking the verification successful. The user won't get any idea what was happened because it will redirect the user to the login page. When you have set it to 'strict', it will work if you copy the verification link directly in the browser address bar but not if the user clicks the link from the Gmail web client because it uses redirect to track the link.

Ravi Patel
  • 1,875
  • 2
  • 28
  • 41