Feathers-vuex: Nuxt CookieStorage example - authenticated calls on the server do not work

Created on 12 Nov 2019  路  8Comments  路  Source: feathersjs-ecosystem/feathers-vuex

Steps to reproduce

  1. Create a universal nuxt app - yarn create nuxt-app
  2. Follow setup steps to add v2 feathers-vuex code - and Nuxt specific steps (i.e. cookie storage, nuxtServerInit/initAuth)
  3. Create a page e.g. /test and in the fetch() function (called server side) place a call which requires auth:
<script>
import { models } from 'feathers-vuex';
export default {
  async fetch ({ store, params }) {
    const { User } = models.api;
    const res = await User.get(1);
    // Fails. Same call on the client will work.
  }
}
</script>
  1. In a browser, set a valid jwt cookie, visit /test and refresh

Expected behavior

The api call in fetch should be made on the server authenticated and succeed.

Actual behavior

A NotAuthenticated error is thrown.

Notes

  • nuxtServerInit/initAuth is successful in reading the jwt from the cookie and setting it in the store (i.e. state.auth.accessToken is valid)
  • but, it doesn't seem to be used in subsequent api requests made on the server?

In trying to investigate this more, I found that the authenticate action when called on the server also fails:

dispatch('auth/authenticate'); // throws an error - No accessToken found in storage

If the accessToken from the store is passed, this gets further but then bails with a document not defined error.

nuxtServerInit({ commit, dispatch, state }, { req }) {
    return initAuth({
      commit,
      dispatch,
      req,
      moduleName: 'auth',
      cookieName: COOKIE_ACCESS_TOKEN
    }).then(() => {
      dispatch('auth/authenticate', { accessToken: state.auth.accessToken, strategy: 'jwt' })
    });
// throws an error - document is not defined . Same if `feathersClient` is passed to `initAuth()`

The document is not defined error is thrown in cookie-storage.js, since setAccessToken() is called when feathersClient.authenticate() succeeds.

// cookie-storage.js
  value: function _setCookie(value) {
      document.cookie = value;
    }

Which obviously will fail on the server.

I am new to feathers-vuex and trying to evaluate it for use in a project. I've spent a decent amount of time looking through old cases/examples on this topic but cannot seem to find anything definitive which covers this scenario?

Thanks in advance!

documentation nuxt

Most helpful comment

I managed to fix this by tweaking the examples in the docs and studying the internals a bit more. Docs are fine for a Nuxt app in SPA mode, i.e. auth/service calls on the client, but you'll hit problems in universal mode when trying to authenticate the user on the server. This is basically because of the recommendation to use CookieStorage as the auth storage adapter.

TL;DR - if you want a Nuxt/SSR/feathers-vuex setup where you can authenticate the user on the server and make authenticated calls in fetch/asyncData, the example below might be a useful starting point.

Case can be closed - there's no bug here, tho perhaps covering universal setup/gotchas in the docs would be nice! Thanks for hard work on this library.

Tweaks for Nuxt universal mode app (auth on the server):

  • feathersClient auth module uses the MemoryStorage adapter when running on the server and the CookieStorage adapter when running on the client
  • auth/authenticate needs to be called somewhere after initAuth(). I added this in nuxtServerInit()- but it could go elsewhere, i.e. middleware. With the MemoryStorage adapter being used on the server, auth/authenticate doesn't throw the document is not defined error anymore.
  • tweaks to work with the Nuxt module syntax for vuex (the feathers-vuex docs are for the classic mode which is depreciated)

Example:

"@feathersjs/authentication-client": "^4.3.11",
"@feathersjs/rest-client": "^4.3.11",
"cookie-storage": "^5.0.3",
"feathers-client": "^2.4.0",
"feathers-hooks-common": "^4.20.7",
"feathers-vuex": "^2.2.4",
"nuxt": "^2.0.0"
// feathers-client.js
import feathers from '@feathersjs/feathers';
import rest from '@feathersjs/rest-client';
import authClient, { MemoryStorage } from '@feathersjs/authentication-client';
import feathersVuex from 'feathers-vuex';
import axios from 'axios';
import { CookieStorage } from 'cookie-storage';
import config from './config';
import { COOKIE_ACCESS_TOKEN } from './modules/auth/persistence/constants';

const host = config.get('API_URL'); // set the feathersjs API server
const restClient = rest(host);
const feathersClient = feathers()
  .configure(restClient.axios(axios))
  .configure(
    authClient({
      // use in memory storage for SSR (safe, won't error out trying to write cookies to document) 
      // use cookies on the client (which are SSR friendly and read for each client request in nuxtServerInit())
      storage: process.server ? new MemoryStorage() : new CookieStorage(),
      storageKey: COOKIE_ACCESS_TOKEN // e.g. feathers-jwt
    })
  );

export default feathersClient;

// Setup feathers-vuex
const {
  makeServicePlugin,
  makeAuthPlugin,
  BaseModel,
  models,
  clients,
  FeathersVuex
} = feathersVuex(feathersClient, {
  serverAlias: 'api', // or whatever that makes sense for your project
  idField: 'id' // `id` and `_id` are both supported, so this is only necessary if you're using something else.
});

export {
  makeAuthPlugin,
  makeServicePlugin,
  BaseModel,
  models,
  clients,
  FeathersVuex
};



md5-bbab6bcc1c0c9def5a388cabf46f1a9f



// src/store/index.js
// updated to use Nuxt module syntax for vuex (not classic mode)
import { initAuth } from 'feathers-vuex';
import { COOKIE_ACCESS_TOKEN } from '../modules/auth/persistence/constants';
import auth from './auth';

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 plugins = [...servicePlugins, auth];
export const actions = {
  nuxtServerInit({ commit, dispatch, state }, { req }) {
    return initAuth({
      commit,
      dispatch,
      req,
      moduleName: 'auth',
      cookieName: COOKIE_ACCESS_TOKEN
    }).then(async () => {
      if (state.auth.accessToken) {
        // auth the current user from the JWT token obtained above
        await dispatch('auth/authenticate', {
          accessToken: state.auth.accessToken,
          strategy: 'jwt'
        });
      }
    });
  }
};



md5-bbab6bcc1c0c9def5a388cabf46f1a9f



// /src/store/auth/index.js
import { makeAuthPlugin } from '../../feathers-client';

export default makeAuthPlugin({
  userService: 'users', // specify the userService to automatically populate the user upon login.
  state: {
    entityIdField: 'id', // The property in the payload storing the user id
    responseEntityField: 'user' // The property in the payload storing the user
  }
});



md5-bbab6bcc1c0c9def5a388cabf46f1a9f





md5-dee5327669a9e3fb6177f3d0e734aa4b


All 8 comments

I managed to fix this by tweaking the examples in the docs and studying the internals a bit more. Docs are fine for a Nuxt app in SPA mode, i.e. auth/service calls on the client, but you'll hit problems in universal mode when trying to authenticate the user on the server. This is basically because of the recommendation to use CookieStorage as the auth storage adapter.

TL;DR - if you want a Nuxt/SSR/feathers-vuex setup where you can authenticate the user on the server and make authenticated calls in fetch/asyncData, the example below might be a useful starting point.

Case can be closed - there's no bug here, tho perhaps covering universal setup/gotchas in the docs would be nice! Thanks for hard work on this library.

Tweaks for Nuxt universal mode app (auth on the server):

  • feathersClient auth module uses the MemoryStorage adapter when running on the server and the CookieStorage adapter when running on the client
  • auth/authenticate needs to be called somewhere after initAuth(). I added this in nuxtServerInit()- but it could go elsewhere, i.e. middleware. With the MemoryStorage adapter being used on the server, auth/authenticate doesn't throw the document is not defined error anymore.
  • tweaks to work with the Nuxt module syntax for vuex (the feathers-vuex docs are for the classic mode which is depreciated)

Example:

"@feathersjs/authentication-client": "^4.3.11",
"@feathersjs/rest-client": "^4.3.11",
"cookie-storage": "^5.0.3",
"feathers-client": "^2.4.0",
"feathers-hooks-common": "^4.20.7",
"feathers-vuex": "^2.2.4",
"nuxt": "^2.0.0"
// feathers-client.js
import feathers from '@feathersjs/feathers';
import rest from '@feathersjs/rest-client';
import authClient, { MemoryStorage } from '@feathersjs/authentication-client';
import feathersVuex from 'feathers-vuex';
import axios from 'axios';
import { CookieStorage } from 'cookie-storage';
import config from './config';
import { COOKIE_ACCESS_TOKEN } from './modules/auth/persistence/constants';

const host = config.get('API_URL'); // set the feathersjs API server
const restClient = rest(host);
const feathersClient = feathers()
  .configure(restClient.axios(axios))
  .configure(
    authClient({
      // use in memory storage for SSR (safe, won't error out trying to write cookies to document) 
      // use cookies on the client (which are SSR friendly and read for each client request in nuxtServerInit())
      storage: process.server ? new MemoryStorage() : new CookieStorage(),
      storageKey: COOKIE_ACCESS_TOKEN // e.g. feathers-jwt
    })
  );

export default feathersClient;

// Setup feathers-vuex
const {
  makeServicePlugin,
  makeAuthPlugin,
  BaseModel,
  models,
  clients,
  FeathersVuex
} = feathersVuex(feathersClient, {
  serverAlias: 'api', // or whatever that makes sense for your project
  idField: 'id' // `id` and `_id` are both supported, so this is only necessary if you're using something else.
});

export {
  makeAuthPlugin,
  makeServicePlugin,
  BaseModel,
  models,
  clients,
  FeathersVuex
};



md5-bbab6bcc1c0c9def5a388cabf46f1a9f



// src/store/index.js
// updated to use Nuxt module syntax for vuex (not classic mode)
import { initAuth } from 'feathers-vuex';
import { COOKIE_ACCESS_TOKEN } from '../modules/auth/persistence/constants';
import auth from './auth';

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 plugins = [...servicePlugins, auth];
export const actions = {
  nuxtServerInit({ commit, dispatch, state }, { req }) {
    return initAuth({
      commit,
      dispatch,
      req,
      moduleName: 'auth',
      cookieName: COOKIE_ACCESS_TOKEN
    }).then(async () => {
      if (state.auth.accessToken) {
        // auth the current user from the JWT token obtained above
        await dispatch('auth/authenticate', {
          accessToken: state.auth.accessToken,
          strategy: 'jwt'
        });
      }
    });
  }
};



md5-bbab6bcc1c0c9def5a388cabf46f1a9f



// /src/store/auth/index.js
import { makeAuthPlugin } from '../../feathers-client';

export default makeAuthPlugin({
  userService: 'users', // specify the userService to automatically populate the user upon login.
  state: {
    entityIdField: 'id', // The property in the payload storing the user id
    responseEntityField: 'user' // The property in the payload storing the user
  }
});



md5-bbab6bcc1c0c9def5a388cabf46f1a9f





md5-dee5327669a9e3fb6177f3d0e734aa4b


Thanks for sharing. We definitely need example apps for both SPA mode and SSR mode.

I still get strange behavior when refreshing the page. i have this setup:

//featherjs-client.js

import { CookieStorage } from 'cookie-storage';
import feathers from '@feathersjs/feathers';
import socketio from '@feathersjs/socketio-client';
import auth from '@feathersjs/authentication-client';
import io from 'socket.io-client';
import feathersVuex from 'feathers-vuex';

const socket = io('http://localhost:3030/', {
  transports: ['websocket'],
  forceNew: true
});
const feathersClient = feathers();

// Setup the transport (Rest, Socket, etc.) here
feathersClient.configure(socketio(socket));

// Available options are listed in the "Options" section
feathersClient.configure(
  auth({
    storageKey: 'tomisha-jwt',
    storage: new CookieStorage()
  })
);

export default feathersClient;

// Setting up feathers-vuex
const {
  makeServicePlugin,
  makeAuthPlugin,
  BaseModel,
  models,
  FeathersVuex
} = feathersVuex(feathersClient, {
  serverAlias: 'api',
  idField: '_id',
  whitelist: ['$regex', '$options']
});

export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex };
src/store/index.js

import Vuex from 'vuex';
import feathersVuex, { initAuth } from 'feathers-vuex';
import feathersClient from '~/utils/feathersClient';
import users from '@/store/services/users';

const { makeAuthPlugin } = feathersVuex(feathersClient);

const createStore = () => {
  return new Vuex.Store({
    state: {},
    mutations: {},
    actions: {
      nuxtServerInit({ commit, dispatch }, { req }) {
        return initAuth({
          commit,
          dispatch,
          req,
          moduleName: 'auth',
          cookieName: 'tomisha-jwt'
        });
      }
    },
    plugins: [
      users,
      makeAuthPlugin({
        state: {
          publicPages: ['login', 'register']
        }
      })
    ]
  });
};

export default createStore;



md5-bbab6bcc1c0c9def5a388cabf46f1a9f



src/store/store.auth.js

import { makeAuthPlugin } from '~/utils/feathersClient';

export default makeAuthPlugin({ userService: 'users' });



md5-bbab6bcc1c0c9def5a388cabf46f1a9f




/src/store/services/user.js

import feathersClient, {
  makeServicePlugin,
  BaseModel
} from '@/utils/feathersClient';

class User extends BaseModel {
  // eslint-disable-next-line no-useless-constructor
  constructor(data, options) {
    super(data, options);
  }
  // Required for $FeathersVuex plugin to work after production transpile.
  static modelName = 'User';
  // Define default properties here
  static instanceDefaults() {
    return {
      email: '',
      password: ''
    };
  }
}
const servicePath = 'users';
const servicePlugin = makeServicePlugin({
  Model: User,
  service: feathersClient.service(servicePath),
  servicePath
});

// Setup the client-side Feathers hooks.
feathersClient.service(servicePath).hooks({
  before: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },
  after: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },
  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
});

export default servicePlugin;



md5-bbab6bcc1c0c9def5a388cabf46f1a9f




middleware/auth.js

export default function(context) {
  const { store, redirect, route } = context;
  const { auth } = store.state;

  if (!auth.publicPages.includes(route.name) && !auth.payload) {
    return redirect('/login');
  }
}



md5-bbab6bcc1c0c9def5a388cabf46f1a9f




pages/users.js

<template>
  <v-row>
    <v-col v-if="!isFindUsersPending" cols="12" class="mx-auto">
      <h1>
        Only authenticated users can see this
      </h1>
      <pre v-for="user in users" :key="user._id">{{ user }}</pre>
    </v-col>
    <v-col v-if="isFindUsersPending" cols="4" class="mx-auto text-center">
      <v-progress-circular size="100" indeterminate />
    </v-col>
  </v-row>
</template>



md5-2afbc0dab9e85e6b1afcec36a3fe7585





md5-581e401552197680e84fac8d53efba0b



 async mounted() {
    try {
      await this.login();
    } catch (error) {
      this.$router.push('/login');
    }
  },
  methods: {
    ...mapActions('auth', { login: 'authenticate' })
  },

in /pages/users.js all is good again. But i don't want to do this on every page where i need to make authenticated req.
Is this a bug or is this normal behavior?

Frustration level 2000%. :(
Did i miss something in the setup?
Can someone help?

in /pages/users.js all is good again. But i don't want to do this on every page where i need to make authenticated req.

In short, move dispatch('auth/authenticate') to somewhere outside of a single page component (pages/users.js) to somewhere where it runs earlier in the request life cycle (e.g. in nuxtServerInit, a plugin, or a layout). There are pros/cons to each approach - it really depends on what your needs are.

A plugin example for the client, assuming you read the cookie in nuxtServerInit():

// plugins/auth.client.js

// remember to register this plugin in nuxt.config.js
// https://nuxtjs.org/guide/plugins/

export default async function({ store }) {
 // here we will attempt to authenticate the user against the API
 // based upon the token in the store set by nuxtServerInit()
  if (store.state.auth.accessToken) {
    await store.dispatch('auth/authenticate', {
      accessToken: store.state.auth.accessToken,
      strategy: 'jwt'
    });
  }
}

However, if you are running in universal mode and need to make authenticated calls in fetch()/asyncData(), the above solution will not work. In that case, you need to authenticate the user on the server. See the example in the 2nd comment in this case.

// Setup the transport (Rest, Socket, etc.) here
feathersClient.configure(socketio(socket));

I would maybe consider using REST on the server and Socket.io on the client. Socket.io for SSR is much harder to get working and may unnecessarily complicate things here.

I'm closing this now that we have a full Nuxt example in the docs. Thanks!

OMG that it helpful article

Sorry but it feels like the docs still lack Nuxt SSR example, as the docs still do the auth on the client side , not the server side. and the end point being on another domain doesn't affect Nuxt receiving the cookies (because Nuxt is the one using the cookie). please correct me if I'm wrong.

For anyone who is reading this later, I found a short chat going on featherjs slack and a response from marshall that confirms it that doing auth on SSR has problems currently:
just checking here for a sanity check. We just found that calling auth/authenticate inside nuxtServerInit whilst nuxt is in production mode will cause the nuxt SSR to cache the response or something, then issuing users with incorrect JWTs. (thankfully we caught this before it got to production)
So I think that the best solution is going for client side auth for now.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tguelcan picture tguelcan  路  6Comments

gustojs picture gustojs  路  7Comments

coderinblack08 picture coderinblack08  路  7Comments

juancampuzano picture juancampuzano  路  4Comments

Heartnett picture Heartnett  路  4Comments