How To Implement a GraphQL Authentication Scheme In Nuxt.js

Nuxt.js has a great module that makes Authentication very easy in a Nuxt.js application. It comes with the most commonly used authentication providers out of the box, including, Auth0, Discord, Facebook, Google, and so on.

I, however, recently needed to use a GraphQL-based authentication endpoint in a Nuxt.js application, but there was no out-of-the-box support for this yet. Luckily the Nuxt.js authentication module allows for easy extension/customization. So I implemented a solution myself. And in this post, I will walk you through how I achieved it.

Now, in this post, I assume the reader already knows the basics of how GraphQL works, and also the basics of using the Nuxt.js auth-module.

So the goal is to implement a solution that allows us to authenticate a Nuxt.js application using a GraphQL mutation like;

mutation LoginMutation($email: String, $password: String) {
  logIn(email: $email, password: $password) {
    token
    expiresIn
  }
}

The challenge, however, is the default Nuxt.js auth schemes use Axios to send auth requests, but unlike traditional endpoints, GraphQL endpoints do not use a library like Axios, they use GraphQL clients such as Apollo Client to send requests.

So, what we are going to do in order to authenticate with a GraphQL endpoint is to override relevant methods in the already existing Local scheme in the Nuxt.js auth module to allow them use the apollo client to make authentication requests rather than using Axios.

Nuxt.js also has a great module for working with apollo called @nuxtjs/apollo. And we also need the graphql-tag npm package to help parse raw GraphQL mutations like the above. I'm assuming you already have the @nuxtjs/auth module installed and set up in your project. If you don't, this is a good time to install all of these packages.

npm install --save @nuxtjs/auth-next @nuxtjs/apollo graphql-tag

# OR

yarn add @nuxtjs/auth-next @nuxtjs/apollo graphql-tag

Then add the modules to your nuxt.config.js file, and configure your GraphQL endpoint in the apollo config;

// nuxt.config.js

export default {
  // ...
  modules: [
    // ...
    '@nuxtjs/auth-next',
    '@nuxtjs/apollo',
  ],

  auth: {
    // Options
  }

  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:4000/graphql', // Your graphql endpiont
      }
    }
  }
}

See the @nuxtjs/apollo documentation for more configuration options.

Also, be sure to activate the Vuex store if you haven’t already, by creating an index.js file under the store folder as the auth module will not work without it.

Active the Vuex store

Now once the @nuxtjs/apollo module is installed and set up, it adds two important variables under the app object in the Nuxt.js context; apolloProvider and $apolloHelpers.

  • apolloProvider will be used to access our apollo client so that we can use it to make auth requests inside our custom scheme

  • And $apolloHelpers will be used to set and reset the auth token for the apollo module itself, so that GraphQL requests are authenticated when the user is logged in.

Now we are ready to start building our custom scheme.

First, let's create a new folder in our root directory and name it schemes, then create a new file named graphqlScheme.js inside it.

Create a schemes folder and a graphqlScheme.js file

Next, copy and paste the foundation for our custom scheme into the graqhqlScheme.js file;

// ~/schemes/graphqlScheme.js

import { gql } from 'graphql-tag'

import { LocalScheme } from '~auth/runtime'

const LOGIN_MUTATION = gql`
  mutation LoginMutation($email: String, $password: String) {
    logIn(email: $email, password: $password) {
      token
      expiresIn
    }
  }
`

export const LOGOUT_MUTATION = gql`
  mutation LogOutMutation {
    logOut
  }
`

export const USER_DETAILS_QUERY = gql`
  query UserDetailsQuery {
    me {
      id
      name
      email
    }
  }
`

export default class GraphQLScheme extends LocalScheme {
  // TODO: Override relevant LocalScheme methods
}

We first define our GraphQL mutations and user details query, passing them through the qql function from the graphql-tag npm package. Finally, we define a class for our custom scheme.

Note: The structure/shape of your mutations and query depends on your back-end implementation. And in this post, we are focusing on the front-end. But if you have any questions about a specific back-end implementation, feel free to drop them in the comment section below and I will do my best to answer them.

Now, let's begin to write out our custom scheme, starting with our login method;

The login method accepts two arguments. The first one is the credentials which holds the username/email and password submitted by the user and the second argument is the auth options, and the only option we are interested in, for now, is the reset option, which we will set to true by default.

async login(credentials, { reset = true } = {}) {
  // ...
}

Next, we need to access the apolloProvider and the $apolloHelpers variables from the Nuxt.js context so we can make the authentication request. We have access to the Nuxt.js context in our scheme as this.$auth.ctx. Also, apollo can have multiple clients, so we need to specify that we need the defaultClient, then use ES6 syntax to rename the variable to apolloClient.

async login(credentials, { reset = true } = {}) {
  const {
    apolloProvider: { defaultClient: apolloClient },
    $apolloHelpers,
  } = this.$auth.ctx.app

  // ...
}

The next part is to determine if we want to reset leftover tokens before attempting to log in.

async login(credentials, { reset = true } = {}) {
  const {
    apolloProvider: { defaultClient: apolloClient },
    $apolloHelpers,
  } = this.$auth.ctx.app

  // Ditch any leftover local tokens before attempting to log in
  if (reset) {
    this.$auth.reset({ resetInterceptor: false })
  }

  // ...
}

Finally, for the login method, we make the login request, update our token set in the cookie, and fetch the authenticated user. The finished login method looks like this;

async login(credentials, { reset = true } = {}) {
  const {
    apolloProvider: { defaultClient: apolloClient },
    $apolloHelpers,
  } = this.$auth.ctx.app

  // Ditch any leftover local tokens before attempting to log in
  if (reset) {
    this.$auth.reset({ resetInterceptor: false })
  }

  // Make login request
  const response = await apolloClient
    .mutate({
      mutation: LOGIN_MUTATION,
      variables: credentials,
    })
    .then(({ data }) => data && data.logIn)
  
  // Update our cookie token
  this.token.set(response.token)

  // Set our graphql-token so subsequent graphql request are authenticated
  await $apolloHelpers.onLogin(response.token)

  // Fetch user
  await this.fetchUser() // We will define this function next

  return response.token
}

Next, let's define our fetchUser method.

fetchUser() {
  const {
    apolloProvider: { defaultClient: apolloClient },
  } = this.$auth.ctx.app

  // Token is required but not available
  if (!this.check().valid) {
    return
  }

  // Try to fetch user
  return apolloClient
    .query({
      query: USER_DETAILS_QUERY,
    })
    .then(({ data }) => {
      if (!data.me) {
        const error = new Error(`User Data response not resolved`)
        return Promise.reject(error)
      }
      // Set the auth user
      this.$auth.setUser(data.me)

      return data.me
    })
    .catch((error) => {
      this.$auth.callOnError(error, { method: 'fetchUser' })
      return Promise.reject(error)
    })
}

This method is pretty straightforward, just like the login method, we first access the apollo defaultClient, next, we check if we have a valid token set, we then fetch the authenticated user using our USER_DETAILS_QUERY, and finally, use the auth module's setUser() method to set the logged in user.

Next, our logout method;

async logout() {
  const {
    apolloProvider: { defaultClient: apolloClient },
    $apolloHelpers,
  } = this.$auth.ctx.app

  await apolloClient
    .mutate({
      mutation: LOGOUT_MUTATION,
    })
    .catch(() => {
      // Handle errors
    })

  // Reset regardless
  $apolloHelpers.onLogout()
  return this.$auth.reset({ resetInterceptor: false })
}

We must call the $apolloHelpers.onLogout() function after we have logged the user out, to let the @nuxtjs/apollo module know that the user is no longer authenticated. It is also important to set the resetInterceptor option to false when calling the auth module's reset() method. You only want to set the option to true if you are using Axios, but we are not using Axios, so setting it to true will raise errors, especially if we don't have the Axios module installed in our project.

The next function we need to override is initializeRequestInterceptor. This function is called when the scheme is initialized. What it does is basically initialize Axios interceptors. But again, since we are not using Axios, we need to override it to avoid raising errors.

initializeRequestInterceptor() {
  // Instead of initializing axios interceptors, Do nothing
  // Since we are not using axios
}

And finally, let's override the reset method, which is pretty straightforward as well. We set the logged in user to false again using the auth module's setUser() method, then we reset our token.

reset() {
  this.$auth.setUser(false)
  this.token.reset()
}

So the finished version of our custom scheme looks like this;

import { gql } from 'graphql-tag'

import { LocalScheme } from '~auth/runtime'

const LOGIN_MUTATION = gql`
  mutation LoginMutation($email: String, $password: String) {
    logIn(email: $email, password: $password) {
      token
      expiresIn
    }
  }
`

export const LOGOUT_MUTATION = gql`
  mutation LogOutMutation {
    logOut
  }
`

export const USER_DETAILS_QUERY = gql`
  query UserDetailsQuery {
    me {
      id
      name
      email
    }
  }
`

export default class GraphQLScheme extends LocalScheme {
  async login(credentials, { reset = true } = {}) {
    const {
      apolloProvider: { defaultClient: apolloClient },
      $apolloHelpers,
    } = this.$auth.ctx.app

    // Ditch any leftover local tokens before attempting to log in
    if (reset) {
      this.$auth.reset({ resetInterceptor: false })
    }

    // Make login request
    const response = await apolloClient
      .mutate({
        mutation: LOGIN_MUTATION,
        variables: credentials,
      })
      .then(({ data }) => data && data.logIn)

    this.token.set(response.token)

    // Set your graphql-token
    await $apolloHelpers.onLogin(response.token)

    // Fetch user
    await this.fetchUser()

    // Update tokens
    return response.token
  }

  fetchUser() {
    const {
      apolloProvider: { defaultClient: apolloClient },
    } = this.$auth.ctx.app

    // Token is required but not available
    if (!this.check().valid) {
      return
    }

    // Try to fetch user
    return apolloClient
      .query({
        query: USER_DETAILS_QUERY,
      })
      .then(({ data }) => {
        if (!data.me) {
          const error = new Error(`User Data response not resolved`)
          return Promise.reject(error)
        }

        this.$auth.setUser(data.me)

        return data.me
      })
      .catch((error) => {
        this.$auth.callOnError(error, { method: 'fetchUser' })
        return Promise.reject(error)
      })
  }

  async logout() {
    const {
      apolloProvider: { defaultClient: apolloClient },
      $apolloHelpers,
    } = this.$auth.ctx.app

    await apolloClient
      .mutate({
        mutation: LOGOUT_MUTATION,
      })
      .catch(() => {
        // Handle errors
      })

    // But reset regardless
    $apolloHelpers.onLogout()
    return this.$auth.reset({ resetInterceptor: false })
  }

  initializeRequestInterceptor() {
    // Instead of initializing axios interceptors, Do nothing
    // Since we are not using axios
  }

  reset() {
    this.$auth.setUser(false)
    this.token.reset()
  }
}

Now that our GraphQL scheme is all done, let's configure it in our nuxt.config.js file.

// nuxt.config.js

auth: {
  strategies: {
    graphql: {
      scheme: '~/schemes/graphqlScheme.js',
    },
  },
  redirect: {
    login: '/login',
    logout: '/login?logout=true',
    callback: false,
    home: '/dashboard',
  },
},

And that’s it.

To log in using our new strategy, assuming our email address is [email protected] and our password is [email protected], we simply do;

const credentials = { email: '[email protected]', password: '[email protected]' }

this.$auth.loginWith('graphql', credentials)

To see it in action, let's create a basic login and a dashboard pages in our project;

Create a login and dashboard page in your nuxt.js project

In the login.vue page, let's create a basic login form using bootstrap-vue components;

<template>
  <b-container>
    <b-row>
      <b-col md="4" offset-md="4" class="mt-5">
        <form method="POST" @submit.prevent="handleLoginSubmit">
          <div class="form-group">
            <label for="email">Email address</label>
            <input
              id="email"
              v-model="form.email"
              type="email"
              class="form-control"
              aria-describedby="emailHelp"
            />
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input
              id="password"
              v-model="form.password"
              type="password"
              class="form-control"
            />
          </div>

          <button type="submit" class="btn btn-primary" :disabled="formBusy">
            <b-spinner v-if="formBusy" small class="mr-2" /> Login
          </button>
        </form>
      </b-col>
    </b-row>
  </b-container>
</template>

<script>
export default {
  middleware: 'auth',

  auth: 'guest',
}
</script>

From the above template, you can tell we need to create the form and formBusy variables and more importantly the handleLoginSubmit method. So let's go ahead and do that in the script section;

<script>
export default {
  middleware: 'auth',

  auth: 'guest',

  data() {
    return {
      form: {
        email: '',
        password: '',
      },
      formBusy: false,
    }
  },

  methods: {
    async handleLoginSubmit() {
      const credentials = this.form
      this.formBusy = true

      try {
        // Using our custom strategy 
        await this.$auth.loginWith('graphql', credentials)

        this.formBusy = false
      } catch (errors) {
        this.formBusy = false
        // Handle errors
      }
    },
  },
}
</script>

For the dashboard.vue page, we are going to use the auth module's $auth.user variable to display the name of the logged-in user in the navbar. We will then implement a logout button.

<template>
  <div class="dashboard-page">
    <b-navbar toggleable="lg" type="dark" variant="primary">
      <b-navbar-brand href="#">Nuxt-graphql-auth</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav class="ml-auto">
          <b-nav-item-dropdown right>
            <template #button-content>
              {{ $auth.user.name }}
            </template>
            <b-dropdown-item @click="handleLogout">Sign Out</b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>

    <b-container>
      <b-row>
        <b-col md="10" offset-md="1" class="mt-5">
          <h1>Welcome to the dashboard</h1>
        </b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
export default {
  middleware: 'auth',

  methods: {
    async handleLogout() {
      this.$nuxt.$loading.start()
      await this.$auth.logout()
    }
  },
}
</script>

Now if you navigate to the /login page and enter the correct email and password combination (according to your GraphQL backend/server), you should be logged in, and redirected to the /dashboard page.

Nuxt.js GraphQL log in demo

Backend Implementation

Check out my post on How To Authenticate a Nuxt.js App With a Laravel GraphQL Backend to learn more about the backend implementation.

And that’s it for this post.

You can find the complete source code used in this tutorial here.

I hope this was helpful. As I said, if you have any questions, drop them in a comment below and I will do my best to answer them.

If you made it this far, 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. ✌🏽