How To Authenticate a Nuxt.js App With a Laravel GraphQL Backend

Laravel GraphQL Nuxt.js auth

Laravel, GraphQL, and Nuxt.js are meant for each other. When used together in a project, they make development easy, expressive, and powerful.

It may not, however, be obvious how to go about combining these frameworks in a project, especially for beginners. So in this post, I will walk you through how to set up a GraphQL authentication API built on top of Laravel. And how to set up a basic Nuxt.js app to consume the API.

The setup requires that the backend and the frontend be separated and only communicate through API requests. Essentially, we are going to have two repositories; one for Laravel-GraphQL API, and the other for the frontend, which is a Nuxt.js app.

I have already covered most of the frontend aspects of the project in my previous post; How To Implement a GraghQL Authentication Scheme In Nuxt.js. If you haven’t read that post, please go through it first, and then come back to this one because we are going to build on what is covered in there.

For the backend, we are going to be using Lighthouse, a framework that will help us create a GraphQL API on top of Laravel.

Let's go ahead and create the Laravel project.

Using the Laravel installer, let's install Laravel;

laravel new nuxt-laravel-backend

Once it's done installing, change directory to the newly created nuxt-laravel-backend folder and install the nuwave/lighthouse package in our project via composer;

cd nuxt-laravel-backend

composer require nuwave/lighthouse

Then publish the Lighthouse default schema using the following artisan command:

php artisan vendor:publish --tag=lighthouse-schema

Open config/cors.php and add graphql to the paths array;

// ...

'paths' => ['graphql', /* ... */ ],

// ...

Authenticating With JWT

Of course, you can use standard Laravel mechanisms to authenticate users of your GraphQL API, but that means you need to have a separate non-GraphQL /login and /logout routes, and in the case where every other endpoint in your application is GraphQL-based, it may seem a little out of place to have only your authentication endpoint be something else.

Also, by having all GraphQL-based endpoints, we eliminate the need to use an additional package like Axios in our project altogether.

So, for our authentication, we are going to be using JSON Web Tokens, which is a secure mechanism for transmitting information between parties. In this case our frontend and our backend.

Laravel has a great package that makes using JWT for authentication very easy. The package is tymon/jwt-auth.

Let's Install tymon/jwt-auth via composer

composer require tymon/jwt-auth

Next, publish the config/jwt.php configuration file using the vendor:publish Artisan command.

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Then, generate the jwt secret by running the following command:

php artisan jwt:secret

This will add a JWT_SECRET variable to your .env file.

Next, we need to implement the Tymon\JWTAuth\Contracts\JWTSubject contract on our User model, which requires that we implement the two methods getJWTIdentifier() and getJWTCustomClaims().

The example below should give you an idea of how this could look. You should make any changes, as necessary, to suit your own needs.

<?php

namespace App\Models;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Inside the config/auth.php file we will need to make a few changes to configure Laravel to use the jwt guard to power our application authentication.

We need to tell the api guard to use the jwt driver and then set the api guard as the default.

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

// ...

'guards' => [
    // ...
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

The Authentication Schema

The next step is to open graphql/schema.graphql file and add our authentication schema;

# ...

type Query {
    # ...
    me: User @auth(guard: "api")
}

type Mutation {
    logIn (
        email: String @rules(apply: ["required"])
        password: String @rules(apply: ["required"])
    ): AuthResponse @field(resolver: "Auth\\LoginMutator")

    logOut: Boolean @field(resolver: "Auth\\LogoutMutator")
}

type User {
    id: ID!
    name: String!
    email: String!
}

type AuthResponse {
    token: String
    expiresIn: String
}

Here we have the me GraphQL query, which returns the user details when they have logged in (Notice the @auth directive).

Then we have two mutations; logIn and logOut. The logIn mutation of course accepts two arguments, email and password. These two mutations will be resolved by two custom resolvers - which we'll create in a bit - specified by the @field directive.

Finally, we have the AuthResponse and User types which is what we return on successful authentication, and when we run the me query, respectively.

Let's generate our mutation resolvers next.

Run the following Artisan commands to generate our LoginMutator and our LogoutMutator resolvers;

php artisan lighthouse:mutation Auth/LoginMutator

php artisan lighthouse:mutation Auth/LogoutMutator

The above command will generate the skeleton for the two resolvers and place them inside an app/GraphQL/mutations/Auth folder.

Handling Invalid Credentials

Before we go ahead and start writing out the actual code for the mutation resolvers we have just generated, let's talk about how to handle errors when the credentials submitted by the user are invalid.

There's no other reliable way of returning an error message when using GraphQL apart from throwing an exception. And by default, Lighthouse does not relay exception error messages to the client/user to prevent information from leaking. In our case, however, we want to display an explicit error message to the user when they submit an invalid credential.

Thankfully, there's an \Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions interface, that can be implemented by exceptions to control how they are rendered to the client.

Let's create a custom exception type that will allow us to pass information to the client about the reason the exception was thrown. And let's call the exception RuntimeValidationException.

Run the following Artisan command to generate a new exception class in our app/Exceptions folder;

php artisan make:exception RuntimeValidationException

Next, import the \Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions interface in the just-generated exception file, and make sure our new exception class implements it;

<?php

namespace App\Exceptions;

use Exception;
use Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions;

class RuntimeValidationException extends Exception implements RendersErrorsExtensions
{
    // ..
}

When we throw this exception, we want to be able to pass a key that will be passed down to the client so we can determine what kind of error it is.

So, let’s define a private property called $key in the exception class, then we make sure it is passed when throwing the exception, along with the exception message, and we do that by specifying them as arguments in the class' __construct() method.

/**
* @var @string
*/
private $key;

public function __construct(string $message, string $key)
{
    parent::__construct($message);

    $this->key = $key;
}

Then, we let Lighthouse know that this exception is safe to be shown to a client by returning true from the isClientSafe() function. We use the getCategory() method to describe the category of the error, and last but definitely not least, we use the extensionsContent() method, which allows us to customize what will be rendered to the client, to return the value of our $key variable.

Here's a complete version of the RuntimeValidationException class;

<?php

namespace App\Exceptions;

use Exception;
use Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions;

class RuntimeValidationException extends Exception implements RendersErrorsExtensions
{
    /**
    * @var @string
    */
    private $key;

    public function __construct(string $message, string $key)
    {
        parent::__construct($message);

        $this->key = $key;
    }

    /**
     * Returns true when exception message is safe to be displayed to a client.
     *
     * @api
     * @return bool
     */
    public function isClientSafe(): bool
    {
        return true;
    }

    /**
     * Returns string describing a category of the error.
     *
     * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
     *
     * @api
     * @return string
     */
    public function getCategory(): string
    {
        return 'runtime-validation';
    }

    /**
     * Return the content that is put in the "extensions" part
     * of the returned error.
     *
     * @return array
     */
    public function extensionsContent(): array
    {
        return [
            'key' => $this->key,
        ];
    }
}

Now if we throw this exception in our query or mutation like this;

throw new RuntimeValidationException('Custom error message', 'errorKey');

A mutation or query that the error was thrown in will render like this to the client:

{
  "data": null,
  "errors": [
    {
      "message": "Custom error message",
      "extensions": {
        "category": "runtime-validation",
        "key": "errorKey",
      }
    }
  ]
}

Nice!

Writing the mutation resolvers

Let's begin writing out our mutation resolvers starting with the LoginMutator.

From our project root folder, open app/GraphQL/Mutations/Auth/LoginMutator.php and make the following changes:

First, retrieve the submitted user credentials from the $args variable;

$credentials = [
  'email' => $args['email'],
  'password' => $args['password']
];

Next, we check if the credentials are valid using the auth guard's attempt() method. The method will return an access token if the credentials are valid or false if they are not. If the credentials are not valid we throw our custom RuntimeValidationException, and if they are, we simply return the token along with an expiry time.

Here's a complete version of LoginMutator.php;

<?php

namespace App\GraphQL\Mutations\Auth;

use App\Exceptions\RuntimeValidationException;

class LoginMutator
{
    /**
     * @param  null  $_
     * @param  array<string, mixed>  $args
     */
    public function __invoke($_, array $args)
    {
        $credentials = [
            'email' => $args['email'],
            'password' => $args['password']
        ];

        $token = auth('api')->attempt($credentials);

        if (! $token) {
            throw new RuntimeValidationException(
                'Email or password is incorrect',
                'InvalidCredentials'
            );
        }

        return [
            'token' => $token,
            'expiresIn' => auth()->factory()->getTTL() * 60
        ];
    }
}

For our LogoutMutator.php, we simply call auth()->logout(), which will return true if successful and false otherwise. Pretty straightforward;

<?php

namespace App\GraphQL\Mutations\Auth;

class LogoutMutator
{
    /**
     * @param  null  $_
     * @param  array<string, mixed>  $args
     */
    public function __invoke($_, array $args)
    {
        return auth()->logout();
    }
}

And that's pretty much it for our backend.

Now we can start the backend server by running the serve Artisan command;

php artisan serve

Then take note of our backend URL;

Laravel development URL

The Frontend

As I said, we have already covered most of the frontend aspects in my previous post; How To Implement a GraghQL Authentication Scheme In Nuxt.js. You can find the frontend repository we created in that post here.

Now we just need to make some changes to our frontend to make it connect and work with our backend.

First, let's modify our nuxt.config.js file, changing our graphql endpoint URL to our backend URL.

// ...

apollo: {
  clientConfigs: {
    default: {
      httpEndpoint: 'http://127.0.0.1:8000/graphql', // Your graphql endpiont
    }
  }
},

// ...

Next, we modify our login method in our login.vue file to handle our custom errors;

Make sure there's an errors variable in the data method in our login.vue component;

data() {
  return {
    // ...
    errors: [],
  }
}

Then modify the handleLoginSubmit method like so;

async handleLoginSubmit() {
  const credentials = this.form
  this.formBusy = true
  this.errors = []

  try {
    await this.$auth.loginWith('graphql', credentials)

    this.formBusy = false
  } catch ({ graphQLErrors: errors }) {
    this.formBusy = false
    // Handle hour custom error
    if (errors && errors.length) {
      errors.forEach((error) => {
        if (error.extensions.key === 'InvalidCredentials') {
          this.errors.push(error.message)
        }
      })
    } else {
      // Handle other errors
    }
  }
}

Here we catch the graphQLErrors and check if it has an error with the InvalidCredentials key which we specified in our backend's LoginMutator. If it does we add the error message to our errors array.

Let's display the errors to the user using bootstrap-vue's b-alert component.

Add the following above <b-form> in the template section of the login.vue component

<b-alert v-if="errors.length" variant="danger" dismissible show>
  <p v-for="(error, i) in errors" :key="i + 1" class="m-0">
    {{ error }}
  </p>
</b-alert>

Now if we try to log in using an invalid credential, we should get a nice error message displayed to us.

Invalid credentials demo

To test a valid credential, we can quickly create a new user in our Laravel project using a Laravel seeder.

Open database/seeders/DatabaseSeeder.php from your Laravel project's root folder, and modify the run() method like so;

public function run()
{
    // \App\Models\User::factory(10)->create();

    \App\Models\User::factory()->create([
        'email' => '[email protected]',
        'password' => bcrypt('classified'),
    ]);
}

Now run the db:seed Artisan command;

php artisan db:seed

And now trying to log in with the email [email protected] and password classified should log you in successfully.

Laravel GraphQL nuxt.js auth demo

And that's it.

The complete source code for the backend can be found here.

I hope you found this post helpful. If you did, I write posts like this every week where I share some of the things I've learned. Consider following me on Twitter @nzesalem so you know when I publish the next one. ✌🏽