Skip to content

Custom scheme for GraphQL with refresh token #1825

@rowphant

Description

@rowphant

Hi!
Im quite new to Vue and Nuxt. So far I like the framework until...I got stuck with user authentication. ^^
I made different JWT authentications with React and Next.js in the past and never had bigger problems. Now that I'm using Nuxt with the auth-module I'm kind of frustrated and dont know what to do anymore. The documentation (especially about custom schemes) seems to be incomplete. At least I just couldnt find how to properly refresh tokens.
But Im still optmtistic, that's why I made this post.

For the backend Im using the following:

  • Wordpress
  • WP GraphQL Plugin
  • WPGraphQL JWT Authentication Plugin

Due to GraphQL the frontend cannot use Axios but Apollo instead to make GraphQL Queries/Mutations (right?). After some research I found out how to create a custom scheme and was able to make a login which is redirecting to '/dashboard'. That's cool and I thought my Job was done here. But it wasn't. 5 Minutes (or 300 seconds) after logging in, the token was probably expired and I was redirected back to the login page. For any reason this.check().valid was still true and Idk why.
Anyway this must be the point where I need to refresh the token but I just dont get it working. So Im hoping anyone can help me here. To be honest I dont even know where to trigger the refresh function exactly. In my code I try to refresh the token when user details cannot be loaded which leads to a GraphQL internal server error and I think it's not even the correct way to trigger the token refresh. 🙈 Please forgive me my messy code. I'm still learning.

Thanks in advance if there is anybody out there who can help 💐


This is the code used for the authentication:

nuxt.config.js

modules: [
  '@nuxtjs/auth-next',
  '@nuxtjs/apollo',
],
auth: {
    strategies: {
      graphql: {
        scheme: '@/schemes/graphqlScheme.js',
        token: {
          property: false,
          maxAge: 300,
        },
        autoLogout: false,
      },
    },
    redirect: {
      login: '/login',
      logout: '/login',
      home: '/dashboard',
    },
}

graphqlScheme.js

import { gql } from 'graphql-tag'
import { RefreshScheme, RefreshController, RefreshToken } from '~auth/runtime'

const LOGIN_MUTATION = gql`
  mutation LoginMutation($user: String!, $password: String!) {
    login(input: { username: $user, password: $password }) {
      authToken
      refreshToken
    }
  }
`

export const USER_DETAILS_QUERY = gql`
  query UserDetailsQuery {
    viewer {
      id
      username
      jwtAuthToken
      jwtAuthExpiration
      jwtRefreshToken
    }
  }
`

const REFRESH_JWT_AUTH_TOKEN = gql`
  mutation RefreshJwtAuthToken($refreshToken: String!) {
    refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
      authToken
    }
  }
`

class CustomRefreshTokenController extends RefreshController {
  async handleRefresh() {
    const refreshToken = this.scheme.refreshToken.get()

    const {
      apolloProvider: {
        clients: { authConfig: apolloClient },
      },
      $apolloHelpers,
    } = this.$auth.ctx.app

    return apolloClient
      .mutate({
        mutation: REFRESH_JWT_AUTH_TOKEN,
        variables: { refreshToken: refreshToken },
        fetchPolicy: 'no-cache',
      })
      .then((res) => {
        const refreshedToken = res?.data?.refreshJwtAuthToken?.authToken

        this.scheme.token.set(refreshedToken)

        // Set your graphql-token
        // $apolloHelpers.onLogin(login.authToken)

        // Fetch user
        // this.$auth.fetchUser()
      })
      .catch((error) => {
        console.log(error)
      })
  }
}

export default class GraphQLScheme extends RefreshScheme {
  constructor(...params) {
    super(...params)

    // This option will prevent $axios methods from being called
    // since we are not using axios
    this.options.token.global = false

    // Initialize Refresh Token instance
    this.refreshToken = new RefreshToken(this, this.$auth.$storage)

    // Add token refresh support
    this.refreshController = new CustomRefreshTokenController(this)
  }

  async login(credentials, { reset = true } = {}) {
    // this.$auth.logState = 'Logging in'
    const {
      apolloProvider: {
        clients: { authConfig: apolloClient },
      },
      $apolloHelpers,
      // $config,
    } = 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,
    })

    const login = response?.data?.login

    login && console.log('Login successful: ', login)
    !login && console.log('Login error')

    this.$auth.logState = 'Login successful'

    this.token.set(login.authToken)
    this.$auth.setUserToken(login.authToken, login.refreshToken)

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

    // Fetch user
    // await this.fetchUser()

    // Update tokens
    return login.authToken
  }

  // Override `fetchUser` method of `local` scheme
  fetchUser() {
    console.log('fetching User details...', this)
    // this.$auth.logState = 'Loading user'

    const {
      apolloProvider: {
        clients: { authConfig: 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,
        fetchPolicy: 'no-cache', // Important for authentication!
      })
      .then(({ data }) => {
        if (!data.viewer) {
          const error = new Error(`User Data response not resolved`)
          return Promise.reject(error)
        }

        this.$auth.setUser(data.viewer)

        return data
      })
      .catch((error) => {
        console.log('Error @fetchUser')
        this.$auth.refreshTokens()
        // this.$auth.callOnError(error, { method: 'fetchUser' })
        // return Promise.reject(error)
      })
  }

  async logout() {
    const { $apolloHelpers } = this.$auth.ctx.app

    $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()
    this.refreshToken.reset()
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions