feathers-vuex icon indicating copy to clipboard operation
feathers-vuex copied to clipboard

Nuxt auth plugin not initiating after page refresh

Open mategvo opened this issue 5 years ago • 13 comments

Steps to reproduce

(First please check that this issue is not already solved as described here)

  • [ ] Tell us what broke. The more detailed the better.
  • [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc.

Expected behavior

If user is signed in, a page refresh should keep them logged in.

Actual behavior

User is not authenticated, regardless of jwt-token being stored in localStorage or cookies

System configuration

Latest nuxt js and feathers vuex Node feathers API

nuxt.config.js:

  modules: [
    // Doc: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/pwa',
    // Doc: https://github.com/nuxt-community/dotenv-module
    '@nuxtjs/dotenv',
    '@nuxtjs/toast',
    // '@nuxtjs/auth'
    'nuxt-client-init-module'
  ],

store/index.js (as per official documentation)

// ~/store/index.js
import {
  makeAuthPlugin,
  initAuth,
  hydrateApi,
  models
} from '~/plugins/feathers'
const auth = makeAuthPlugin({
  userService: 'users',
  state: {
    publicPages: [
      'login',
      'signup',
      'index',
      'terms-of-service',
      'privacy-policy'
    ]
  },
  actions: {
    onInitAuth({ state, dispatch }, payload) {
      if (payload) {
        dispatch('authenticate', {
          strategy: 'jwt',
          accessToken: state.accessToken
        })
          .then((result) => {
            // handle success like a boss
            console.log('loged in')
          })
          .catch((error) => {
            // handle error like a boss
            console.log(error)
          })
      }
    }
  }
})

const requireModule = require.context(
  // The path where the service modules live
  './services',
  // Whether to look in subfolders
  false,
  // Only include .js files (prevents duplicate imports`)
  /.js$/
)
const servicePlugins = requireModule
  .keys()
  .map((modulePath) => requireModule(modulePath).default)

export const modules = {
  // Custom modules
}

export const state = () => ({
  // Custom state
})

export const mutations = {
  // Custom mutations
}

export const actions = {
  // Custom actions
  nuxtServerInit({ commit, dispatch }, { req }) {
    return initAuth({
      commit,
      dispatch,
      req,
      moduleName: 'auth',
      cookieName: 'feathers-jwt'
    })
  },
  nuxtClientInit({ state, dispatch, commit }, context) {
    hydrateApi({ api: models.api })

    if (state.auth.accessToken) {
      return dispatch('auth/onInitAuth', state.auth.payload)
    }
  }
}

export const getters = {
  // Custom getters
}

export const plugins = [...servicePlugins, auth]

Module versions (especially the part that's not working):

"@feathersjs/authentication-client": "^4.5.1",
"@feathersjs/configuration": "^2.0.6",
"@feathersjs/express": "^1.3.1",
"@feathersjs/feathers": "^3.3.1",
"@feathersjs/rest-client": "^4.5.1",
"@feathersjs/socketio-client": "^4.5.1",
"@nuxtjs/axios": "^5.9.5",
"@nuxtjs/dotenv": "^1.4.0",
"@nuxtjs/pwa": "^3.0.0-0",
"@nuxtjs/toast": "^3.3.0",
"@vue/composition-api": "^0.4.0",
"bootstrap": "^4.1.3",
"bootstrap-vue": "^2.0.0",
"consola": "^2.11.3",
"cookie-storage": "^6.0.0",
"cross-env": "^5.2.0",
"feathers-hooks-common": "^5.0.2",
"feathers-vuex": "^3.6.1",
"nuxt": "^2.0.0",
"nuxt-client-init-module": "^0.1.8",
"socket.io-client": "^2.3.0"

NodeJS version: v12.15.0

Operating System:

macos & docker

Browser Version: Latest Chrome

React Native Version: no react

Module Loader: NPM

mategvo avatar Feb 26 '20 14:02 mategvo

However picks this up, many thanks in advance for support. We are currenlty solving the problem in default.vue template

  async created() {
    await this.authenticate()
      .then(() => {
        // todo - redirect to where the user came from
        this.$router.push('/events')
      })
      .catch((e) => {
        console.log('not authenticated')
        this.logout()
      })
  },

but we are now having problems with redirects.

mategvo avatar Feb 26 '20 14:02 mategvo

Could this be related to this issue?

marssantoso avatar Feb 26 '20 20:02 marssantoso

Same problem with the lastest version of feathers-vuex and nuxt SSR

itoonx avatar Mar 02 '20 17:03 itoonx

I've created a plugin to solve this problem. It works with localstorage. Needs to be edited slighltly for cookie storage

// ~/plugins/authInit.js
const storedToken = typeof localStorage['feathers-jwt'] !== 'undefined'
const hashTokenAvailable = window.location.hash.indexOf('access_token' > -1)

export default async (context) => {
  if (
    (!context.app.store.state.auth.user && storedToken) ||
    hashTokenAvailable
  ) {
    console.log('Authenticating', context.app.store.state.auth.user)
    await context.app.store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated', context.app.store.state.auth.user)
      })
      .catch((e) => {
        console.error(e)
      })
  }
}

Remember to initialize it in nuxt.config.js

// nuxt.config.js
  plugins: [
    { src: '~/plugins/authInit.js', ssr: false }
  ],

Here's the version for cookies-storage

// ~/plugins/authInit.js
import { CookieStorage } from 'cookie-storage'

const cookieStorage = new CookieStorage()
const cookie = cookieStorage.getItem('feathers-jwt') !== null
const hashTokenAvailable = window.location.hash.indexOf('access_token' > -1)

export default async (context) => {
  if ((!context.app.store.state.auth.user && cookie) || hashTokenAvailable) {
    console.log('Authenticating', context.app.store.state.auth.user)
    await context.app.store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated', context.app.store.state.auth.user)
      })
      .catch((e) => {
        console.error(e)
      })
  }
}

Hope this helps

mategvo avatar Mar 03 '20 10:03 mategvo

@mateuszgwozdz could you find some time to add your solution to what you see as the best place for it in the Feathers-Vuex Nuxt docs? I've been doing more and more work with Gridsome's pre-rendering, so I'm a little out of the loop with Nuxt. No worries if not. I will leave this open.

marshallswain avatar Mar 12 '20 20:03 marshallswain

Sure, I will do it with pleasure. I just thought that something is not working as supposed to for me, rather than it's a missing functionality

mategvo avatar Mar 13 '20 06:03 mategvo

I put this in the docs in a quite early place, related to auth. It's becuase I assumed this functionality is included and actually lost significant amount of time trying to "fix" it, find a bug. I think it will prevent others from misunderstanding this feature. Still I believe this is something that should work out-of-the-box. I can also write the plugin in the way that automatically determines whether we are using localstorage or cookie and restores the session - would that be useful as out-of-the-box feature?

https://github.com/feathersjs-ecosystem/feathers-vuex/compare/master...mateuszgwozdz:patch-1

mategvo avatar Mar 13 '20 06:03 mategvo

@mateuszgwozdz Unfortunately that patch doesn't work for me with using https://vuex.feathersjs.com/nuxt.html#full-nuxt-configuration-example.

andrewharvey avatar Mar 16 '20 03:03 andrewharvey

Interestingly though I can only replicate this with nuxt (dev mode) not when running on production with nuxt build && nuxt start.

andrewharvey avatar Mar 16 '20 03:03 andrewharvey

Doesn't work for you with SSR you mean? My project is a simple SPA, I forgot to mention I haven't tested SSR, I don't think it will work

mategvo avatar Mar 16 '20 09:03 mategvo

I can confirm it works for me fine in nuxt start. I reload live website and I am still authenticated

mategvo avatar Mar 16 '20 09:03 mategvo

Doesn't work for you with SSR you mean? My project is a simple SPA, I forgot to mention I haven't tested SSR, I don't think it will work

That's right I'm using 'universal' mode with Nuxt, but only when in dev mode not production do I see the issue anyway.

andrewharvey avatar Mar 16 '20 10:03 andrewharvey

Hey guys I wanted to share a workaround that worked for me to get authenticated server side whereas @mategvo's solution will only work client side which can cause problems.

The root of the problem comes from the fact that cookie-storage requires document to work properly which will not work server side to parse the feathers-jwt token.

Compounding that fact is that plugins/feathers from the full nuxt example has .configure(auth({ storage })) which isn't yet aware of the req variable required to parse cookies server side so I cooked up a workaround. Bear in mind that this isn't fully tested yet so there could be some gotchas further down the road so if you see one let me know.

First off, replace cookie-storage with universal-cookie

Then give it a decorate it with the required methods

class CookieStorage extends Cookies {
  get getItem() { return this.get; }
  get setItem() { return this.set; }
  get removeItem()  { return this.remove; }
}

Then, install cookie-universal-nuxt and refactor @mategvo 's code so that it will work on the server side too.

Here's my full workaround:

// ~/plugins/authInit.js
import { storage } from './feathers';

const hashTokenAvailable = process.client && window.location.hash.indexOf('access_token' > -1);

export default async ({ store, $cookie }) => {
  // Give the cookie to the auth module's storage instance
  const cookie = $cookie.get('feathers-jwt');
  if (cookie) {
    storage.set('feathers-jwt', cookie);
  }

  if ((!store.state.auth.user && cookie) || hashTokenAvailable) {
    await store
      .dispatch('auth/authenticate')
      .then(() => {
        console.log('Authenticated');
      })
      .catch((e) => {
        console.error(e)
      })
  }
}
// ~/plugins/feathers.js
import feathers from '@feathersjs/feathers'
import rest from '@feathersjs/rest-client'
import axios from 'axios'
import socketio from '@feathersjs/socketio-client'
import auth from '@feathersjs/authentication-client'
import io from 'socket.io-client'
import Cookies from 'universal-cookie';
import { iff, discard } from 'feathers-hooks-common'
import feathersVuex, { initAuth, hydrateApi } from 'feathers-vuex'

const apiUrl = process.env.API_URL;

let socket
let restClient
// We won't use socket to comunicate from server to server
if (process.client) {
  socket = io(apiUrl, { transports: ['websocket'] })
} else {
  restClient = rest(apiUrl)
}
const transport = process.client ? socketio(socket) : restClient.axios(axios)

class CookieStorage extends Cookies {
  get getItem() { return this.get; }
  get setItem() { return this.set; }
  get removeItem()  { return this.remove; }
  // and any other required method as needed
}
const storage = new CookieStorage()

const feathersClient = feathers()
  .configure(transport)
  .configure(auth({ storage }))
  .hooks({
    before: {
      all: [
        iff(
          context => ['create', 'update', 'patch'].includes(context.method),
          discard('__id', '__isTemp')
        )
      ]
    }
  })

export default feathersClient

// Setting up feathers-vuex
const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex(
  feathersClient,
  {
    serverAlias: 'api', // optional for working with multiple APIs (this is the default value)
    idField: '_id', // Must match the id field in your database table/collection
    whitelist: ['$regex', '$options'],
    enableEvents: process.client // Prevent memory leak
  }
)

export {
  makeAuthPlugin,
  makeServicePlugin,
  BaseModel,
  models,
  FeathersVuex,
  initAuth,
  hydrateApi,
  storage,
}

Obviously not the best solution here; hoping this SSR oversight gets fixed later.

ghost avatar Apr 01 '21 12:04 ghost