javascript - How to authenticate Nuxt on server side? - Stack Overflow

I have spent the night looking for solutions to this issue, it seems like a lot of people have it and t

I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.

I have JWT for authentication, using the JWTSessions gem for Rails.

On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:

export default function ({ $auth, route, redirect }) {
  const role = $auth.user && $auth.user.role

  if (route.meta[0].requiredRole !== role) {
    redirect('/login')
  }
}

The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.

However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.

I have the following relevant files.

nuxt.config.js

...
  plugins: [
    // ...
    { src: '~/plugins/axios' },
    // ...
  ],

  // ...

  modules: [
    'cookie-universal-nuxt',
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],

  // ...

  axios: {
    baseURL: process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3000/v1',
    credentials: true
  },
  auth: {
    strategies: {
      jwtSessions: {
        _scheme: '~/plugins/auth-jwt-scheme.js',
        endpoints: {
          login: { url: '/signin', method: 'post', propertyName: 'csrf' },
          logout: { url: '/signin', method: 'delete' },
          user: { url: '/users/active', method: 'get', propertyName: false }
        },
        tokenRequired: true,
        tokenType: false
      }
    },
    cookie: {
      options: {
        maxAge: 64800,
        secure: process.env.NODE_ENV === 'production'
      }
    }
  },

auth-jwt-scheme.js

const tokenOptions = {
  tokenRequired: true,
  tokenType: false,
  globalToken: true,
  tokenName: 'X-CSRF-TOKEN'
}

export default class LocalScheme {
  constructor (auth, options) {
    this.$auth = auth
    this.name = options._name
    this.options = Object.assign({}, tokenOptions, options)
  }

  _setToken (token) {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
    }
  }

  _clearToken () {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
      this.$auth.ctx.app.$axios.setHeader('Authorization', false)
    }
  }

  mounted () {
    if (this.options.tokenRequired) {
      const token = this.$auth.syncToken(this.name)
      this._setToken(token)
    }

    return this.$auth.fetchUserOnce()
  }

  async login (endpoint) {
    if (!this.options.endpoints.login) {
      return
    }

    await this._logoutLocally()

    const result = await this.$auth.request(
      endpoint,
      this.options.endpoints.login
    )

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + result
        : result

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async setUserToken (tokenValue) {
    await this._logoutLocally()

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + tokenValue
        : tokenValue

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async fetchUser (endpoint) {
    if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
      return
    }

    if (!this.options.endpoints.user) {
      this.$auth.setUser({})
      return
    }

    const user = await this.$auth.requestWith(
      this.name,
      endpoint,
      this.options.endpoints.user
    )
    this.$auth.setUser(user)
  }

  async logout (endpoint) {
    if (this.options.endpoints.logout) {
      await this.$auth
        .requestWith(this.name, endpoint, this.options.endpoints.logout)
        .catch(() => {})
    }

    return this._logoutLocally()
  }

  async _logoutLocally () {
    if (this.options.tokenRequired) {
      this._clearToken()
    }

    return await this.$auth.reset()
  }
}

axios.js

export default function (context) {
  const { app, $axios, redirect } = context

  $axios.onResponseError(async (error) => {
    const response = error.response
    const originalRequest = response.config

    const access = app.$cookies.get('jwt_access')
    const csrf = originalRequest.headers['X-CSRF-TOKEN']

    const credentialed = (process.client && csrf) || (process.server && access)

    if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
      if (process.server) {
        $axios.setHeader('X-CSRF-TOKEN', csrf)
        $axios.setHeader('Authorization', access)
      }

      const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })

      if (newToken.data.csrf) {
        $axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
        $axios.setHeader('Authorization', newToken.data.access)

        if (app.$auth) {
          app.$auth.setToken('jwt_access', newToken.data.csrf)
          app.$auth.syncToken('jwt_access')
        }

        originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
        originalRequest.headers.Authorization = newToken.data.access

        if (process.server) {
          app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
        }

        return $axios(originalRequest)
      } else {
        if (app.$auth) {
          app.$auth.logout()
        }
        redirect(301, '/login')
      }
    } else {
      return Promise.reject(error)
    }
  })
}

This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.

I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.

I have JWT for authentication, using the JWTSessions gem for Rails.

On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:

export default function ({ $auth, route, redirect }) {
  const role = $auth.user && $auth.user.role

  if (route.meta[0].requiredRole !== role) {
    redirect('/login')
  }
}

The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.

However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.

I have the following relevant files.

nuxt.config.js

...
  plugins: [
    // ...
    { src: '~/plugins/axios' },
    // ...
  ],

  // ...

  modules: [
    'cookie-universal-nuxt',
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],

  // ...

  axios: {
    baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example./v1' : 'http://localhost:3000/v1',
    credentials: true
  },
  auth: {
    strategies: {
      jwtSessions: {
        _scheme: '~/plugins/auth-jwt-scheme.js',
        endpoints: {
          login: { url: '/signin', method: 'post', propertyName: 'csrf' },
          logout: { url: '/signin', method: 'delete' },
          user: { url: '/users/active', method: 'get', propertyName: false }
        },
        tokenRequired: true,
        tokenType: false
      }
    },
    cookie: {
      options: {
        maxAge: 64800,
        secure: process.env.NODE_ENV === 'production'
      }
    }
  },

auth-jwt-scheme.js

const tokenOptions = {
  tokenRequired: true,
  tokenType: false,
  globalToken: true,
  tokenName: 'X-CSRF-TOKEN'
}

export default class LocalScheme {
  constructor (auth, options) {
    this.$auth = auth
    this.name = options._name
    this.options = Object.assign({}, tokenOptions, options)
  }

  _setToken (token) {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
    }
  }

  _clearToken () {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
      this.$auth.ctx.app.$axios.setHeader('Authorization', false)
    }
  }

  mounted () {
    if (this.options.tokenRequired) {
      const token = this.$auth.syncToken(this.name)
      this._setToken(token)
    }

    return this.$auth.fetchUserOnce()
  }

  async login (endpoint) {
    if (!this.options.endpoints.login) {
      return
    }

    await this._logoutLocally()

    const result = await this.$auth.request(
      endpoint,
      this.options.endpoints.login
    )

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + result
        : result

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async setUserToken (tokenValue) {
    await this._logoutLocally()

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + tokenValue
        : tokenValue

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async fetchUser (endpoint) {
    if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
      return
    }

    if (!this.options.endpoints.user) {
      this.$auth.setUser({})
      return
    }

    const user = await this.$auth.requestWith(
      this.name,
      endpoint,
      this.options.endpoints.user
    )
    this.$auth.setUser(user)
  }

  async logout (endpoint) {
    if (this.options.endpoints.logout) {
      await this.$auth
        .requestWith(this.name, endpoint, this.options.endpoints.logout)
        .catch(() => {})
    }

    return this._logoutLocally()
  }

  async _logoutLocally () {
    if (this.options.tokenRequired) {
      this._clearToken()
    }

    return await this.$auth.reset()
  }
}

axios.js

export default function (context) {
  const { app, $axios, redirect } = context

  $axios.onResponseError(async (error) => {
    const response = error.response
    const originalRequest = response.config

    const access = app.$cookies.get('jwt_access')
    const csrf = originalRequest.headers['X-CSRF-TOKEN']

    const credentialed = (process.client && csrf) || (process.server && access)

    if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
      if (process.server) {
        $axios.setHeader('X-CSRF-TOKEN', csrf)
        $axios.setHeader('Authorization', access)
      }

      const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })

      if (newToken.data.csrf) {
        $axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
        $axios.setHeader('Authorization', newToken.data.access)

        if (app.$auth) {
          app.$auth.setToken('jwt_access', newToken.data.csrf)
          app.$auth.syncToken('jwt_access')
        }

        originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
        originalRequest.headers.Authorization = newToken.data.access

        if (process.server) {
          app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
        }

        return $axios(originalRequest)
      } else {
        if (app.$auth) {
          app.$auth.logout()
        }
        redirect(301, '/login')
      }
    } else {
      return Promise.reject(error)
    }
  })
}

This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.

Share Improve this question asked Oct 11, 2020 at 12:12 zcsereizcserei 6657 silver badges33 bronze badges 1
  • 2 Hello! Did you manage to solve this? I am using session authentication but i have the exact same problem as you stackoverflow./questions/67488337/… – Jack022 Commented May 12, 2021 at 22:11
Add a ment  | 

1 Answer 1

Reset to default 1

In order for You not to lose Your authentication session in the system, You first need to save your JWT token to some storage on the client: localStorage or sessionStorage or as well as token data can be saved in cookies.

For to work of the application will be optimally, You also need to save the token in the store of Nuxt. (Vuex)

If You save Your token only in srore of Nuxt and use only state, then every time You refresh the page, Your token will be reset to zero, since the state will not have time to initialize. Therefore, you are redirected to the page /login.

To prevent this from happening, after you save Your token to some storage, You need to read it and reinitialize it in the special method nuxtServerInit(), in the universal mode his will be work on the server side the very first. (Nuxt2)

Then, accordingly, You use Your token when sending requests to the api server, adding to each request that requires authorization, a header of the Authorization type.

Since Your question is specific to the Nuxt2 version, for this version a working code example using cookies to store the token would be:

/store/auth.js

import jwtDecode from 'jwt-decode'

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

export const getters = {
  isAuthenticated: state => Boolean(state.token),
  token: state => state.token
}

export const mutations = {
  SET_TOKEN (state, token) {
    state.token = token
  }
}

export const actions = {
  autoLogin ({ dispatch }) {
    const token = this.$cookies.get('jwt-token')
    if (isJWTValid(token)) {
      dispatch('setToken', token)
    } else {
      dispatch('logout')
    }
  },
  async login ({ mit, dispatch }, formData) {
    const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
    dispatch('setToken', token)
  },
  logout ({ mit }) {
    this.$axios.setToken(false)
    mit('SET_TOKEN', null)
    this.$cookies.remove('jwt-token')
  },
  setToken ({ mit }, token) {
    this.$axios.setToken(token, 'Bearer')
    mit('SET_TOKEN', token)
    this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
    // <-- above use, for example, moment or add function that will puted date
  }
}

/**
 * Check valid JWT token.
 *
 * @param token
 * @returns {boolean}
 */
function isJWTValid (token) {
  if (!token) {
    return false
  }

  const jwtData = jwtDecode(token) || {}
  const expires = jwtData.exp || 0

  return new Date().getTime() / 1000 < expires
}

/store/index.js

export const state = () => ({
  // ... Your state here
})

export const getters = {
  // ... Your getters here
}

export const mutations = {
  // ... Your mutations here
}

export const actions = {
  nuxtServerInit ({ dispatch }) { // <-- init auth
    dispatch('auth/autoLogin')
  }
}

/middleware/isGuest.js

export default function ({ store, redirect }) {
  if (store.getters['auth/isAuthenticated']) {
    redirect('/admin')
  }
}

/middleware/auth.js

export default function ({ store, redirect }) {
  if (!store.getters['auth/isAuthenticated']) {
    redirect('/login')
  }
}

/pages/login.vue

<template>
  <div>
    <!--    Your template here-->
  </div>
</template>

<script>
export default {
  name: 'Login',
  layout: 'empty',
  middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
  data () {
    return {
      controls: {
        login: '',
        password: ''
      },
      rules: {
        login: [
          { required: true, message: 'login is required', trigger: 'blur' }
        ],
        password: [
          { required: true, message: 'password is required', trigger: 'blur' },
          { min: 6, message: 'minimum 6 length', trigger: 'blur' }
        ]
      }
    }
  },
  head: {
    title: 'Login'
  },
  methods: {
    onSubmit () {
      this.$refs.form.validate(async (valid) => { // <-- Your validate
        if (valid) {
          // here for example: on loader
          try {
            await this.$store.dispatch('auth/login', {
              login: this.controls.login,
              password: this.controls.password
            })
            await this.$router.push('/admin')
          } catch (e) {
            // eslint-disable-next-line no-console
            console.error(e)
          } finally {
            // here for example: off loader
          }
        }
      })
    }
  }
}
</script>

! - You must have the following packages installed:

  • cookie-universal-nuxt
  • jsonwebtoken
  • jwt-decode

I think you will find my answer helpful. If something is not clear, ask!

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745162446a4614448.html

相关推荐

  • javascript - How to authenticate Nuxt on server side? - Stack Overflow

    I have spent the night looking for solutions to this issue, it seems like a lot of people have it and t

    3小时前
    10

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信