Graphql-laravel: Authorize user through mutation

Created on 10 Jun 2019  路  8Comments  路  Source: rebing/graphql-laravel

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?

help wanted question

All 8 comments

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?

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

php 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.

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');
}
  1. Store the token in local storage/cookie
if(data != undefined) {
     const token = data.AuthenticateUserMutation;
     localStorage.setItem('token', token.accessToken);
}
  1. 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}` : "",
    }
  }
});
  1. 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.

@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?

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;

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;
}

@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.

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dongkaipo picture dongkaipo  路  4Comments

stevelacey picture stevelacey  路  8Comments

edgarsn picture edgarsn  路  4Comments

mikev032 picture mikev032  路  4Comments

timothyvictor picture timothyvictor  路  3Comments