serversideup / roastandbrew

Updated content available! We learned a lot since we originally wrote this article. We now have this updated for Laravel 8, Vue, and NuxtJS 👉 https://srvrsi.de/book
https://srvrsi.de/book
MIT License
300 stars 90 forks source link

Switching authentication to passwordless #8

Open iceberg53 opened 6 years ago

iceberg53 commented 6 years ago

Environment

Laravel Homestead

Issue description

I tried to replace the socialite authentication with a passwordless medium style authentication. First, the user provides his email where I send an authentication token (I replaced the social authentication links in the login modal with a form input for email ); Then, after clicking on the authentication link, the user is either created or retrieved by email and redirected to the home page. In the authentication controller previously used for socialite, I check the token validity (existence and expiration ), and I log the user in with Auth::login($user) , $user being the user for the mail address the token came from. I used Auth::login($user) by following the example for socialite authentication in roastandbrew code.

I also faced a bug related to the session storage file as described at https://github.com/laravel/framework/issues/22514 that I bypassed by changing the session driver environment variable to cookie.

After redirecting the user, I noticed that despite the Auth::login($user) action I did, the user didn't appear as authenticated (userLoadStatus==3).

When I checked the API\UsersController.php, I saw that we get the authenticated user with Auth::guard('api')->user(), and I didn't understand why.

I thought that there would be something like Auth::guard('api')->login($user), but it throws an exception for non-existent method.

When I replace Auth::guard('api')->user() with response()->json(Auth::guard('api')->user()), the user appears to be logged in the application whereas a curl call with the following command : curl -X GET -H 'Accept: application/json' http://memoire.local/api/v1/user returns an empty response.

dd(Auth::guard('api')->user()) throws an error when called from my token model dd(Auth::guard()->user()) does return a user.

I got confused, and after searching from yesterday, I decided to open this issue to know if I missed something.

By the way, your code is very fine and easy to understand, Thank you for it and the tutorial.

Steps to reproduce the issue

I mainly followed the steps in this medium article https://medium.com/@hive_adrian/password-less-email-authentication-in-laravel-ecf5388efe74 by adapting it to the single page app context (no blade view, response->json instead of views as return values, etc, ...)

Additional details / screenshots

I want to point out that my authentication routes were initially in the web.php but there was a 419 error which led me to write them inside the routes/api.php file.

Thank you very much !!

danpastori commented 6 years ago

Hi @iceberg53 ,

Thank you very much for the incredibly detailed issue this helps tremendously!

So there's a couple tricks going on here. You are using the new password-less authentication and we are using Laravel Passport. To be honest, I haven't messed around with the password-less authentication yet, but I think I got the core of the concept by reading the article you provided. With Laravel Passport, when you authenticate a user, new API tokens get created. The first thing you will need to do is make sure that your User.php model has the following traits:

<?php
/*
  Define the namespace for the user model.
*/
namespace App\Models;

/*
  Defines the facades used by the model.
*/
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * Defines the user model.
 */
class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

That being the HasApiTokens. This means when your model is authenticated the tokens are created. The awesome thing about Laravel passport and on the web is it stores a cookie. With each request, that cookie gets sent that has the information needed to apply the tokens to the request on behalf of the user. So the guard Auth::guard('api')->user() gets the user based off of the single authentication from the request and uses that user to handle the request. The auth:api middleware that blocks routes checks these tokens before continuing the request.

Essentially, I would check to make sure you have your model with the HasApiTokens and you have the middleware set up correctly. Otherwise your user WILL be authenticated, but the tokens haven't been made and won't be accessible from the API.

Let me know if that helps! If you have any code examples, that'd be helpful.

iceberg53 commented 6 years ago

I already included the HasApiTokens in the user model.

You can find my relevant source files here

I didn't include my config/auth.php because I didn't change anything in the config.

Excuse me for the late reply and once again thank you for the work you did.

danpastori commented 6 years ago

@iceberg53 So I finally got a chance to check out some of the code, sorry bout that! What I think the issue is in Token.php on line 48 you create a new token. I believe this overwrites what Passport creates.

Also, the guard only returns the authorized user if the call to load the user from the guard is after a route that is protected by the 'auth:api' middleware. So the request comes in -> hits the middleware -> middleware checks the tokens -> creates a token -> then the user is authenticated and you can load the user like normal. If it the user load call is not behind this middleware, then you have to check the guard which goes through the process of checking the tokens.

This goes hand in hand with your Token.php file. I'd first remove the new token creation (if focusing on having Passport handle the tokens). At the end of the file when you use the default guard and get a user, that's correct. This is because the request didn't pass through the auth:api middleware.

If you are loading a user form a route that does not pass through the auth:api middleware use: Auth::guard('api')->user()

If you are loading a user from a route that DOES pass through the auth:api middleware use: Auth::user()

Hopefully that helps!