nuxt-community / axios-module

Secure and easy axios integration for Nuxt 2
https://axios.nuxtjs.org
MIT License
1.19k stars 245 forks source link

Axios Bearer Token #40

Closed atmar closed 6 years ago

atmar commented 7 years ago

Hello,

I'd like to grab JWT out of cookie and set it to the axios instance as authorization, but this only works on server side. After initial load, subsequent AsyncData ajax requests throw an error that token is not given.

'check-auth.js' available in nuxt.config router as global middleware

import { getTokenFromCookie, getTokenFromLocalStorage } from '~/utils/auth'
import { setBearerToken } from '~/utils/axiosHeaders'

export default async function ({ isServer, store, req, app }) {
  // If nuxt generate, pass this middleware
  if (isServer && !req) return
  const token = isServer ? getTokenFromCookie(req) : getTokenFromLocalStorage()
  if (token) {
    await setBearerToken(app, token)
    await store.dispatch('auth/set_user')
  }
}

Here I set the bearer token using the 'context.app' as axios instance.

async function setBearerToken(vm, token) {
  return new Promise((resolve, reject) => {
    vm.$axios.setToken(token, 'Bearer')
    resolve(true)
  })
}
  async set_user({ commit }) {
    try {
      const { data } = await this.$axios.get('auth/user')
      commit('SET_USER', data.data)
    } catch (error) {
      // Token doesn't work anymore, throw it out local storage
      unsetToken()
    }
  },

Using this, the user loads perfectly and the initial data on the loaded page aswell, but when I navigate through the app, all asyncData axios requests fail because token is not set. My guess is that the axios instance is completely different on clientside and doen't have the bearer token. But how to solve this?

I used to set token in localstorage and load user after app has loaded, but this brought other issues. (For example if I load the account page (not navigate to it, but fresh refresh), I want to redirect to login page if not logged in using middleware. But if the token is in localstorage, the user is loaded after the middleware and always thinks a user is not logged in. Therefore always redirecting. So I've read that other people use JWT in cookie to grab user beforehand, but this brings me to the above issue

This question is available on Nuxt.js community (#c38)
atmar commented 7 years ago

I am able to get it work with a (to me) hacky method and the help of this. But it doesn't feel right. At first, the middleware reloaded the user on every request. So I've added a seperate function for isServer(). Otherwise it would call "store.dispatch('auth/set_user', req)" on every route change in client and regrab the user. I've created a nuxtClientInit function that loads once with the app on client side (found that somewhere on the forum here a long time ago).

middleware/check-auth.js in nuxt.config.js

export default async function ({ isServer, isClient, store, req, app }) {
  // If nuxt generate, pass this middleware
  if (isServer && !req) return
  if (isServer) {
    await store.dispatch('auth/set_user', req)
  }
}

plugins/nuxt-client-init.js

export default async (ctx) => {
    await ctx.store.dispatch('nuxtClientInit', ctx)
}

store/index.js vuex store

import { initStorage } from '~/localstorage'
import { setGeoData } from '~/utils/geodata'

import { setAnonId } from '~/utils/axiosHeaders'

export const state = () => ({
  imageUrl: process.env.IMAGE_URL
})

export const actions = {
  async nuxtClientInit({ commit }, context) {
    // First fill localstorage with values for new customers
    await initStorage()

    // Then fill Axios headers with anonid if set in local storage
    await setAnonId(this)

    // Set cart data
    context.store.dispatch('cart/set_cart')

    await context.store.dispatch('auth/load_token', null)
    context.store.dispatch('auth/start_refresh_interval')
  },
  async nuxtServerInit({ commit }, context) {
    await setGeoData(context)
    console.log('nuxt server init')
  }
}

store/auth.js vuex store:

import Cookies from 'js-cookie'
import Cookie from 'cookie'

const inBrowser = typeof window !== 'undefined'

export const state = () => ({
  user: null,
  token: null
})

export const mutations = {
  SET_USER: function (state, user) {
    state.user = user
  },
  SET_TOKEN: function (state, token) {
    state.token = token
    // Setup axios
    this.$axios.setToken(token, 'Bearer')

    // Store token in cookies
    if (inBrowser) {
      if (!token) {
        return Cookies.remove('token')
      }
      Cookies.set('token', token)
    }
  }
}

export const getters = {
  isAuthenticated(state) {
    return !!state.user
  }
}

export const actions = {
  async load_token({ commit }, req) {
    // Try to extract token from cookies
    const cookieStr = inBrowser ? document.cookie : req.headers.cookie
    const cookies = Cookie.parse(cookieStr || '') || {}
    const token = cookies.token
    commit('SET_TOKEN', token)
  },
  async register({ commit }, { email, password, confirmPassword }) {
    try {
      const { data } = await this.$axios.post('auth/register', { email, password, confirmPassword })
      commit('SET_USER', data.user)
      commit('SET_TOKEN', data.token)
    } catch (error) {
      throw error
    }
  },
  async login({ commit }, { email, password }) {
    try {
      const { data } = await this.$axios.post('auth/login', { email, password })
      commit('SET_USER', data.user)
      commit('SET_TOKEN', data.token)
    } catch (error) {
      if (error.response && error.response.status === 401) {
        throw new Error('Bad credentials')
      }
      throw error
    }
  },
  async logout({ commit }) {
    await this.$axios.post('auth/logout')
    commit('SET_USER', null)
    commit('SET_TOKEN', null)
  },
  async set_user({ commit, dispatch, state }, req) {
    try {
      await dispatch('load_token', req)
      // No token
      if (!state.token) {
        return
      }
      await _refreshToken(this, commit)
      const { data } = await this.$axios.get('auth/user')
      commit('SET_USER', data.data)
    } catch (error) {
      // Token doesn't work anymore, throw it out
      commit('SET_USER', null)
      commit('SET_TOKEN', null)
    }
  },
  start_refresh_interval({ getters }) {
    let refreshInterval = setInterval(() => {
      // If authenticated, refresh token every 60 min 
      // Get new token from header & replace token in cookies and axios headers
      try {
        if (getters.isAuthenticated) {
          _refreshToken(this)
        }
      } catch (error) {
        clearInterval(refreshInterval)
      }
    }, 1000 * 60 * 60)
  }
}

async function _refreshToken(vm, commit) {
  return vm.$axios.get('auth/refresh')
    .then(async ({ headers }) => {
      let token = headers.authorization.replace('Bearer ', '')
      console.log('new token ' + token)
      commit('SET_TOKEN', token)
    })
    .catch((error) => {
      console.log(error)
    })
}
atmar commented 7 years ago

Had to do some changes, Cookie.set and Cookie.remove doesn't work on server side. I called _refreshToken on serverside, changing the token. But it wasn't saved in the cookie. So now I load user data at server side and refresh the token on client side and set it in the cookie. I got to say, this authentication system is a whole lot more complicated than expected, perhaps there is a better way to do all of this. (backend is JWT Auth Laravel).

store/auth.js vuex store:

import Cookies from 'js-cookie'
import Cookie from 'cookie'

const inBrowser = typeof window !== 'undefined'
const opts = {}
export const state = () => ({
  user: null,
  token: null
})

export const mutations = {
  SET_USER: function (state, user) {
    state.user = user
  },
  SET_TOKEN: function (state, token) {
    state.token = token
    // Setup axios
    this.$axios.setToken(token, 'Bearer')

    // Store token in cookies
    if (inBrowser) {
      if (!token) {
        return Cookies.remove('token', opts)
      }
      Cookies.set('token', token, opts)
    }
  }
}

export const getters = {
  isAuthenticated(state) {
    return !!state.user
  }
}

export const actions = {
  async load_token({ commit }, req) {
    // Try to extract token from cookies
    const cookieStr = inBrowser ? document.cookie : req.headers.cookie
    const cookies = Cookie.parse(cookieStr || '') || {}
    const token = cookies.token
    commit('SET_TOKEN', token)
  },

  async register({ commit }, { email, password, confirmPassword }) {
    try {
      const { data } = await this.$axios.post('auth/register', { email, password, confirmPassword })

      commit('SET_USER', data.user)
      commit('SET_TOKEN', data.token)
    } catch (error) {
      throw error
    }
  },

  async login({ commit }, { email, password }) {
    try {
      const { data } = await this.$axios.post('auth/login', { email, password })
      commit('SET_USER', data.user)
      commit('SET_TOKEN', data.token)
    } catch (error) {
      if (error.response && error.response.status === 401) {
        throw new Error('Bad credentials')
      }
      throw error
    }
  },

  async logout({ commit }) {
    await this.$axios.post('auth/logout')
    commit('SET_USER', null)
    commit('SET_TOKEN', null)
  },

  async set_user({ commit, dispatch, state }, req) {
    try {
      await dispatch('load_token', req)
      // No token
      if (!state.token) {
        return
      }

      if (inBrowser) {
        await _refreshToken(this, commit)
      } else {
        const { data } = await this.$axios.get('auth/user')
        commit('SET_USER', data.data)
      }
    } catch (error) {
      // Token doesn't work anymore, throw it out
      commit('SET_USER', null)
      commit('SET_TOKEN', null)
    }
  },

  start_refresh_interval({ getters }) {
    let refreshInterval = setInterval(() => {
      // If authenticated, refresh token every 60 min 
      // Get new token from header & replace token in storage and axios headers
      try {
        if (getters.isAuthenticated) {
          _refreshToken(this)
        }
      } catch (error) {
        clearInterval(refreshInterval)
      }
    }, 1000 * 60 * 60)
  }
}

async function _refreshToken(vm, commit) {
  return vm.$axios.get('auth/refresh')
    .then(async ({ headers }) => {
      let token = headers.authorization.replace('Bearer ', '')
      commit('SET_TOKEN', token)
    })
    .catch(() => {
      console.log('error')
    })
}
pi0 commented 6 years ago

Hey. With 5.0.0 release many things have been changed. Also, i highly suggest using auth module for Authentication handling.

Please open a new issue if you have more problems.