How to authenticate Nuxt on server side?

Viewed 2962

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.com/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.

0 Answers
Related