Auth-module: Runtime configuration of authentication strategies

Created on 3 Jun 2020  路  12Comments  路  Source: nuxt-community/auth-module

What problem does this feature solve?

This feature allows to configure the auth module at runtime. This facilitates creating applications with the "Build Once, Deploy Many" principle (by applying guideline III of the 12-factor app methodology, storing configuration in the environment). Put simply: This feature enables deployment of the same build in multiple environments (production, staging, different customers), each having a different authentication configuration (different endpoints, providers, schemes, ...).

What does the proposed changes look like?

Change the module's plugin.js template and replace the hardcoded strategy configuration settings.
One possible way to approach this is to implement the dynamic configuration solution that will be available in the upcoming 2.13 release of Nuxt (cfr. https://github.com/nuxt/nuxt.js/issues/5100). Another would be to provide some kind of hook into the module initialisation. The runtime configuration of the auth module should also work when using (asynchronous configuration)[https://nuxtjs.org/guide/configuration/#asynchronous-configuration].

This feature request is available on Nuxt community (#c588)

Most helpful comment

Is there any workaround at the moment, to update/override $auth.strategies at runtime ?

All 12 comments

Is there any workaround at the moment, to update/override $auth.strategies at runtime ?

We are facing the same issue.
Nuxt v2.13 has been released and we hope to use its runtime config feature to overcome this problem with this module.

Same issues here. Any approach on how to do this until the auth module can handle the runtime config?

With the Nuxt v2.13 we do it though the extending auth plugin. Into plugin you can change $auth options to $config options. It works fine as a temporary solution.

Can you give an example on what you exactly changed in the $auth config? Because i can't find any api url there.

Edit:
Okay i got the following and will now test if this is working:
export default function({ $auth, $config }) { $auth.ctx.app.$axios.defaults.baseURL = $config.apiEndpoint }

Edit 2:
this approach is working!

Hello,

I have the same issue. I would like to change the clientId like this and use it later in the nuxt.config.js :

export default {
  mode: 'universal',
  publicRuntimeConfig: {
    CLIENT_ID: process.env.CLIENT_ID
  },
  auth: {
    strategies: {
      aad: {
        scheme: 'oauth2',
        endpoints: {
          authorization: 'https://example.com/connect/authorize',
          token: 'https://example.com/connect/token',
          userInfo: 'https://example.com/connect/userinfo',
          logout: '/'
        },
        clientId: process.env.CLIENT_ID,
        ...
      }
    }

But this method doesn't work.

I try the suggestion of @Organizzzm like this :

In nuxt.config.js, i added this in "auth" section of nuxt.config.js :

plugins: [{ src: '~/plugins/axios', ssr: true }, '~/plugins/auth.js'],

and this is my "~/plugins/auth.js" file :

export default function({ $auth, $config }, inject) {
  $auth.strategies.aad.options.clientId = $config.CLIENT_ID;
}

The clientId is correctly override in GET /connect/authorize request, but not for POST /connect/token request after login redirect.

Thank you for helping me

I attempted to implement the config at runtime as follows

plugins/auth.js

export default function ({ $auth, $config, store }) {
  // Update Strategy at runtime (not working?)
  // https://github.com/nuxt-community/auth-module/issues/713
  $auth.strategies.auth0.options.audience = $config.DGC_AUTH0_AUDIENCE
  $auth.strategies.auth0.options.client_id = $config.DGC_AUTHO_CLIENT_ID
  $auth.strategies.auth0.options.domain = $config.DGC_AUTH0_DOMAIN
  $auth.strategies.auth0.options.userinfo_endpoint = `https://${$config.DGC_AUTH0_DOMAIN}/userinfo`
  $auth.strategies.auth0.options.authorization_endpoint = `https://${$config.DGC_AUTH0_DOMAIN}/authorize`
}

nuxt.config.js

...
[
  '@nuxtjs/auth',
  {
    plugins: [
      '~/plugins/auth.js',
    ],
    redirect: {
      callback: '/callback',
      logout: '/',
    },
    strategies: {
      local: false,
      auth0: {
        domain: process.env.DGC_AUTH0_DOMAIN,
        client_id: process.env.DGC_AUTHO_CLIENT_ID,
        audience: process.env.DGC_AUTH0_AUDIENCE,
      },
    },
  },
]

...

Unfortunately, whilst this does work for some requests, other requests are taking place before the plugin is run, namely the /userinfo request.

So it seems, we do not currently have a working work around

This might help.

Implement custom scheme
Set auth0 scheme to custom scheme in nuxt.config.js

Context is now available in the scheme constructor as auth parameter and anything can be accessed before $auth.init() is called in .nuxt/auth/plugin.js

Annoyingly you need to replicate the OAuth2 scheme due to import filepath error.

scheme/customAuth0Scheme.js

import {
  encodeQuery,
  parseQuery,
  normalizePath,
} from '@nuxtjs/auth/lib/core/utilities'
import nanoid from 'nanoid'
const isHttps = process.server ? require('is-https') : null

const DEFAULTS = {
  token_type: 'Bearer',
  response_type: 'token',
  tokenName: 'Authorization',
  token_key: 'access_token',
  refresh_token_key: 'refresh_token',
}

export default class CustomAuth0Scheme {
  constructor(auth, options) {
    this.$auth = auth
    this.req = auth.ctx.req
    this.name = options._name

    // Update Strategy at runtime
    const domain = this.$auth.ctx.$config.AUTH0_DOMAIN

    this.options = Object.assign({}, DEFAULTS, options, {
      // Update options with runtime configuration
      audience: this.$auth.ctx.$config.AUTH0_AUDIENCE,
      client_id: this.$auth.ctx.$config.AUTH0_CLIENT_ID,
      domain,
      // Update endpoints
      // API documentation https://auth0.com/docs/api/authentication
      userinfo_endpoint: `https://${domain}/userinfo`,
      authorization_endpoint: `https://${domain}/authorize`,
      access_token_endpoint: `https://${domain}/oauth/token`,
      logout_endpoint: `https://${domain}/v2/logout`,
    })
  }

  get _scope() {
    return Array.isArray(this.options.scope)
      ? this.options.scope.join(' ')
      : this.options.scope
  }

  get _redirectURI() {
    const url = this.options.redirect_uri

    if (url) {
      return url
    }

    if (process.server && this.req) {
      const protocol = 'http' + (isHttps(this.req) ? 's' : '') + '://'

      return (
        protocol + this.req.headers.host + this.$auth.options.redirect.callback
      )
    }

    if (process.client) {
      return window.location.origin + this.$auth.options.redirect.callback
    }
  }

  async mounted() {
    // Sync token
    const token = this.$auth.syncToken(this.name)
    // Set axios token
    if (token) {
      this._setToken(token)
    }

    // Handle callbacks on page load
    const redirected = await this._handleCallback()

    if (!redirected) {
      return this.$auth.fetchUserOnce()
    }
  }

  _setToken(token) {
    // Set Authorization token for all axios requests
    this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
  }

  _clearToken() {
    // Clear Authorization token for all axios requests
    this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
  }

  async reset() {
    this._clearToken()

    this.$auth.setUser(false)
    this.$auth.setToken(this.name, false)
    this.$auth.setRefreshToken(this.name, false)

    return Promise.resolve()
  }

  login({ params, state, nonce } = {}) {
    const opts = {
      protocol: 'oauth2',
      response_type: this.options.response_type,
      access_type: this.options.access_type,
      client_id: this.options.client_id,
      redirect_uri: this._redirectURI,
      scope: this._scope,
      // Note: The primary reason for using the state parameter is to mitigate CSRF attacks.
      // https://auth0.com/docs/protocols/oauth2/oauth-state
      state: state || nanoid(),
      ...params,
    }

    if (this.options.audience) {
      opts.audience = this.options.audience
    }

    // Set Nonce Value if response_type contains id_token to mitigate Replay Attacks
    // More Info: https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
    // More Info: https://tools.ietf.org/html/draft-ietf-oauth-v2-threatmodel-06#section-4.6.2
    if (opts.response_type.includes('id_token')) {
      // nanoid auto-generates an URL Friendly, unique Cryptographic string
      // Recommended by Auth0 on https://auth0.com/docs/api-auth/tutorials/nonce
      opts.nonce = nonce || nanoid()
    }

    this.$auth.$storage.setUniversal(this.name + '.state', opts.state)

    const url = this.options.authorization_endpoint + '?' + encodeQuery(opts)

    window.location = url
  }

  logout() {
    this.$auth.reset()

    const opts = {
      client_id: this.options.clientId,
      returnTo: this._logoutRedirectURI,
    }
    const url = `${this.options.logout_endpoint}?${encodeQuery(opts)}`
    window.location.replace(url)
  }

  async fetchUser() {
    if (!this.$auth.getToken(this.name)) {
      return
    }

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

    try {
      const user = await this.$auth.requestWith(this.name, {
        url: this.options.userinfo_endpoint,
      })

      this.$auth.setUser(user)
    } catch (error) {
      if (error.response.status === 401) {
        this.$auth.reset()
        // Set redirect cookie to return user to current page
        this.$auth.$storage.setUniversal(
          'redirect',
          this.$auth.ctx.route.fullPath
        )
        this.$auth.ctx.redirect('/login')
        return
      }
      throw error
    }
  }

  async _handleCallback(uri) {
    // Handle callback only for specified route
    if (
      this.$auth.options.redirect &&
      normalizePath(this.$auth.ctx.route.path) !==
        normalizePath(this.$auth.options.redirect.callback)
    ) {
      return
    }
    // Callback flow is not supported in server side
    if (process.server) {
      return
    }

    const hash = parseQuery(this.$auth.ctx.route.hash.substr(1))
    const parsedQuery = Object.assign({}, this.$auth.ctx.route.query, hash)
    // accessToken/idToken
    let token = parsedQuery[this.options.token_key]
    // refresh token
    let refreshToken = parsedQuery[this.options.refresh_token_key]

    // Validate state
    const state = this.$auth.$storage.getUniversal(this.name + '.state')
    this.$auth.$storage.setUniversal(this.name + '.state', null)
    if (state && parsedQuery.state !== state) {
      return
    }

    // -- Authorization Code Grant --
    if (this.options.response_type === 'code' && parsedQuery.code) {
      const data = await this.$auth.request({
        method: 'post',
        url: this.options.access_token_endpoint,
        baseURL: process.server ? undefined : false,
        data: encodeQuery({
          code: parsedQuery.code,
          client_id: this.options.client_id,
          redirect_uri: this._redirectURI,
          response_type: this.options.response_type,
          audience: this.options.audience,
          grant_type: this.options.grant_type,
        }),
      })

      if (data[this.options.token_key]) {
        token = data[this.options.token_key]
      }

      if (data[this.options.refresh_token_key]) {
        refreshToken = data[this.options.refresh_token_key]
      }
    }

    if (!token || !token.length) {
      return
    }

    // Append token_type
    if (this.options.token_type) {
      token = this.options.token_type + ' ' + token
    }

    // Store token
    this.$auth.setToken(this.name, token)

    // Set axios token
    this._setToken(token)

    // Store refresh token
    if (refreshToken && refreshToken.length) {
      refreshToken = this.options.token_type + ' ' + refreshToken
      this.$auth.setRefreshToken(this.name, refreshToken)
    }

    // Redirect to home
    this.$auth.redirect('home', true)

    return true // True means a redirect happened
  }
}

nuxt.config.js

  '@nuxtjs/auth',
  {
    plugins: ['~/plugins/axios.js', '~/plugins/graphql.js'],
    redirect: {
      callback: '/callback',
      logout: '/',
    },
    strategies: {
      local: false,
      auth0: {
        _scheme: '~/scheme/customAuth0Scheme',
      },
    },
  },

Thanks @ndj91 , I ended up doing something similar: created a custom OAuth2 Scheme that inherits from the packaged one and that merges the static module options with runtime configuration options (available as $auth.ctx.$config).

import merge from 'lodash.merge'
import Oauth2Scheme from '@nuxtjs/auth-next/dist/schemes/oauth2'

export default class CustomOauth2Scheme extends Oauth2Scheme {
  constructor($auth, options) {
      // other preparations here
      const dynamicOptions = merge(
        options,
        $auth.ctx.$config.auth.strategies[options.name]
      )
      super($auth, dynamicOptions)
  }
}

Thanks to both @ndj91 and @studiocredo for sharing your solutions.

To work around the import filepath error mentioned by @ndj91 when using auth module v4, you can do this:

// runtimeConfigurableScheme.js

// This auth plugin will end up in .nuxt/auth/schemes, as will oauth2.js if it's
// also a registered strategy in the module config.
import Oauth2Scheme from './oauth2';

export default class RuntimeConfigurableOauth2Scheme extends Oauth2Scheme {
  constructor($auth, options) {
    const configOptions = {
      ...options,
      ...$auth.ctx.$config.auth.strategies[options['_name']]
    };
    super($auth, configOptions);
  }
}

To ensure oauth2.js exists in the same directory as this custom scheme after build, register it on the module config with:

// nuxt.config.js
module.exports = {
  auth: {
    strategies: {
      local: false,
      oauth2: {
        _scheme: 'oauth2'
      },
      custom: {
        _scheme: '~/path/to/runtimeConfigurableScheme'
      }
    }
  },
  publicRuntimeConfig: {
    auth: {
      strategies: {
        custom: {
          client_id: process.env.AUTH_CLIENT_ID,
          scope: process.env.AUTH_SCOPE
          // ... etc
        }
      }
    }
  }
};

If someone is using the v5 (5.0.0-1613647907.37b1156) of nuxt auth module (https://www.npmjs.com/package/@nuxtjs/auth-next), this config worked for us with Auth0 provider:

// runtimeConfigurableScheme.js

import { Oauth2Scheme } from '@nuxtjs/auth-next/dist/runtime.js'

export default class RuntimeConfigurableOauth2Scheme extends Oauth2Scheme {
  constructor($auth, options) {
    const configOptions = {
      ...options,
      ...$auth.ctx.$config.auth.strategies[options.name],
    }
    super($auth, configOptions)
  }
}
// nuxt.config.js
module.exports = {
  auth: {
    strategies: {
      custom: {
        scheme: '~/path/to/runtimeConfigurableScheme'
      }
    }
  },
  publicRuntimeConfig: {
    auth: {
      strategies: {
        custom: {
          clientId: process.env.NUXT_ENV_AUTH0_CLIENT_ID,
          domain: process.env.NUXT_ENV_AUTH0_DOMAIN ,
          audience: process.env.NUXT_ENV_AUTH0_AUDIENCE,
          logoutRedirectUri: process.env.NUXT_ENV_AUTH0_LOGOUT_REDIRECT',
          scope: ['openid', 'profile', 'email', 'offline_access'],
          responseType: 'code',
          grantType: 'authorization_code',
          endpoints: {
            "authorization": `https://${process.env.NUXT_ENV_AUTH0_DOMAIN}/authorize`,
            "userInfo": `https://${process.env.NUXT_ENV_AUTH0_DOMAIN}/userinfo`,
            "token": `https://${process.env.NUXT_ENV_AUTH0_DOMAIN}/oauth/token`,
            "logout": `https://${process.env.NUXT_ENV_AUTH0_DOMAIN}/v2/logout`
          },
        }
      }
    }
  }
};

Have run into this same issue, with an app that is built once and deployed into multiple environments.

The app is using the Laravel Passport provider (with the password grant type) which isn't available as a scheme that can be extended as in the above comments.

I've also tried the plugin options override approach, but it looks like the client secret is baked into a server middleware by initializePasswordGrantFlow at build time.

鈥ˋny advice welcomed as I'm unsure where best to go from here.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AhmedAtef07 picture AhmedAtef07  路  3Comments

pi0 picture pi0  路  3Comments

Amoki picture Amoki  路  3Comments

weijinnx picture weijinnx  路  3Comments

roosht3 picture roosht3  路  3Comments