Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom scheme for GraphQL with refresh token #1825

Open
rowphant opened this issue Feb 9, 2023 · 1 comment
Open

Custom scheme for GraphQL with refresh token #1825

rowphant opened this issue Feb 9, 2023 · 1 comment
Labels

Comments

@rowphant
Copy link

rowphant commented Feb 9, 2023

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()
  }
}
@davidurco
Copy link

Wow, thanks for asking this question and providing code for refresh token strategy. Exactly what I needed. When it comes to refreshing tokens, it's pretty easy with Axios and how this modules handles it - intercept every request and check if token is valid, if it is, do nothing, if it's not, refresh it and only then continue with the request. This can't be achieved with apollo as far as I know. You have to let every query/mutation pass and only when it fails due to the token being expired you can refresh it and try again. This can be achieved with forward(operation) in apollo global error handler. If backend returns Network Error 401 instead of GraphQLError, forward(operation) is not possible, so in that case you can at least log user out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants