rebing / graphql-laravel

Laravel wrapper for Facebook's GraphQL
MIT License
2.13k stars 266 forks source link

Authorize user through mutation #317

Closed kirgy closed 5 years ago

kirgy commented 5 years ago

I'm trying to authorize a user through a mutation which looks like:

mutation AuthenticateUserMutation($email: String!, $password: String!) {
    AuthenticateUserMutation(email: $email, password: $password) {
        email
    }
}

The class looks like this which works as expected:

class AuthenticateUserMutation extends Mutation
{
    protected $attributes = [
        'name' => 'AuthenticateUserMutation'
    ];

    public function type()
    {
        return GraphQL::type('user');
    }

    public function args()
    {
        return [
            'email' => ['name' => 'email', 'type' => Type::nonNull(Type::string())],
            'password' => ['name' => 'password', 'type' => Type::nonNull(Type::string())]
        ];
    }

    public function rules(array $args = [])
    {
        return [
            'password' => ['required', 'min:8|max:255'],
            'email' => ['required', 'email']
        ];
    }

    public function resolve($root, $args)
    {

       if (! Auth::attempt(['email' => $args['email'], 'password' => $args['password']], true)) {
           return null;
       }

       $user = Auth::user();

       return $user;
    }
}

I'm trying to achieve a Laravel passport based cookie authentication for my front-end ReactJS app, as described here: https://laravel.com/docs/5.5/passport#consuming-your-api-with-javascript

I don't understand how I can achieve this - I have experimented with the config/graphql middleware configuration:

   // ...
    'schemas' => [
        'default' => [
            'query' => [
                'users' => App\GraphQL\Query\UsersQuery::class,
                'me' => App\GraphQL\Query\MeQuery::class,
            ],
            'mutation' => [
                // 'example_mutation'  => ExampleMutation::class,
                'AuthenticateUserMutation' => App\GraphQL\Mutation\AuthenticateUserMutation::class,
            ],
            'method'     => ['get', 'post'],
            'middleware' => [
                \App\Http\Middleware\EncryptCookies::class,
                \Illuminate\Session\Middleware\StartSession::class,
                \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
            ],
        ],
    ],
   // ...

However, Laravel does not seem to want to set the cookie on responses. Can you advise how I can achieve this, or else advise how I should be going about authentication if I'm following the wrong methodology?

mfn commented 5 years ago

I feel I'm not much of a help here, I'm not using passport.

To rule out it has to do with this library, have you double checked the middlewares are executed?

AdrianCarreno commented 5 years ago

I am no expert so my approach may not be the best, but it works. I use React, Passport and GraphQL, and my resolve function for the login is

public function resolve($root, $args)
{
    if (auth()->attempt($args))
        return auth()->user()->createToken('APP_NAME');
    else
        return abort(403, 'Wrong email/password');
}

Where APP_NAME is the name of your client. To login you don't really need a middleware, just follow Passport's instructions for install.

The front end (react) will receive the access token, the expiration date and some other stuff which I don't remember, but the token and the date are the important things. Then you can save them to your cookies or local storage.

kirgy commented 5 years ago

Thanks for your help @AdrianCarreno your answer led me to the right path. I found the following article regarding laravel and SPAs: https://medium.com/@ripoche.b/create-a-spa-with-role-based-authentication-with-laravel-and-vue-js-ac4b260b882f

The article above gives a good overview of the development model of how that looks in a Laravel application.

The short of it is:

  1. Use a mutation to return a Passport token
    public function resolve($root, $args)
    {
    if (auth()->attempt($args))
        return auth()->user()->createToken('APP_NAME');
    else
        return abort(403, 'Wrong email/password');
    }
  2. Store the token in local storage/cookie
    if(data != undefined) {
     const token = data.AuthenticateUserMutation;
     localStorage.setItem('token', token.accessToken);
    }
  3. Set that token as a global default header in your JS library (in my case ReactJS/Apollo/Apollo-auth-link):
    const authLink = setContext((_, { headers }) => {
    // get the authentication token from local storage if it exists
    const token = localStorage.getItem('token');
    // return the headers to the context so httpLink can read them
    return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
    }
    });
  4. carrying on requesting knowing your bearer token will be sent with requests. You'll want to do stuff like checking cookie expirey, sending user to login form etc.
mfn commented 5 years ago

@kirgy it looks to me this is outside the scope of this library to handle.

Or do you have an idea what kind of enhancement could be done to make this easier?

albertcito commented 5 years ago

I did it not using cookies only the token, you have to change the middleware line for this:

'middleware' => ['auth:api'],

And you have to return the token:

$user['token'] = $user->createToken('Todo App')->accessToken;
return $user;
lahed commented 5 years ago

I used this for login

public function resolve($root, $args, SelectFields $fields, ResolveInfo $info)
    {
        if(Auth::attempt(['email' => $args['email'], 'password' => $args['password']])){
            $user = Auth::user();

            return [
                'token' => $user->createToken('mobileApp')->accessToken,
                'data' => $user
            ];
        }else{
            return null;
        }
    }

and on protected resources:

public function authorize(array $args)
    {
        return Auth::guard('api')->check();
    }
public function resolve($root, $args, SelectFields $fields, ResolveInfo $info)
{
   $data = Auth::guard('api')->user();
   //example retrieve name $data->name;
   return $data;
}
kirgy commented 5 years ago

@mfn I don't think anything code-wise in the library needs changing, I think perhaps my issue is just a common developer issue that documentation covering it might solve - or do you feel that's a bit excessive? Happy to write it up if it'll be helpful.

mfn commented 5 years ago

Thanks for getting back!

TBH I'm not sure if such cases are appropriate for the documentation to cover at this point. Because it seems there are 10+1 ways of solving this and none is empirically better than the other.

Documenting one such possibility would give the impression it's the best practice / endorsed approach, but I wouldn't take this stance right now.

Closing this now as the original issue seems solved. Maybe in the future we've more insight and might provide more information in the docs.

Thank you