Runtime configuration of authentication strategies #1597
Replies: 13 comments 3 replies
-
Is there any workaround at the moment, to update/override |
Beta Was this translation helpful? Give feedback.
-
We are facing the same issue. |
Beta Was this translation helpful? Give feedback.
-
Same issues here. Any approach on how to do this until the auth module can handle the runtime config? |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
Can you give an example on what you exactly changed in the $auth config? Because i can't find any api url there. Edit: Edit 2: |
Beta Was this translation helpful? Give feedback.
-
Hello, I have the same issue. I would like to change the clientId like this and use it later in the nuxt.config.js :
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 :
and this is my "~/plugins/auth.js" file :
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 |
Beta Was this translation helpful? Give feedback.
-
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 So it seems, we do not currently have a working work around |
Beta Was this translation helpful? Give feedback.
-
This might help. Implement custom scheme 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',
},
},
}, |
Beta Was this translation helpful? Give feedback.
-
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 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)
}
} |
Beta Was this translation helpful? Give feedback.
-
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
}
}
}
}
}; |
Beta Was this translation helpful? Give feedback.
-
If someone is using the v5 ( // 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`
},
}
}
}
}
}; |
Beta Was this translation helpful? Give feedback.
-
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. Any advice welcomed as I'm unsure where best to go from here. |
Beta Was this translation helpful? Give feedback.
-
In case somebody stumbles upon this problem, (this is specifically for OpenID, but could be applicable for some others)
import { OpenIDConnectScheme } from '@nuxtjs/auth-next/dist/runtime'
import CustomConfigurationDocument from './configDocument'
export default class CustomScheme extends OpenIDConnectScheme {
constructor($auth, options, ...defaults) {
super($auth, options, ...defaults)
// Initialize ConfigurationDocument
this.configurationDocument = new CustomConfigurationDocument(
this,
this.$auth.$storage
)
}
} Doesn't really do anything except for defining a new configuration document. Configuration document:
import { ConfigurationDocument } from '@nuxtjs/auth-next/dist/runtime'
export default class CustomConfigurationDocument extends ConfigurationDocument {
async request() {
// Get Configuration document from state hydration
const serverDoc =
this.scheme.$auth.ctx?.nuxtState?.$auth?.openIDConnect
?.configurationDocument
if (process.client && serverDoc) {
this.set(serverDoc)
}
if (!this.get()) {
const configurationDocument = await this.scheme.requestHandler.axios
.$get('/oidc/')
.catch((e) => Promise.reject(e))
// Push Configuration document to state hydration
if (process.server) {
this.scheme.$auth.ctx.beforeNuxtRender(({ nuxtState }) => {
nuxtState.$auth = {
oidc: {
configurationDocument,
},
}
})
}
this.set(configurationDocument)
}
}
setSchemeEndpoints() {
const configurationDocument = this.get()
this.scheme.options.endpoints = {
authorization: configurationDocument.authorization_endpoint,
token: configurationDocument.token_endpoint,
userInfo: configurationDocument.userinfo_endpoint,
logout: configurationDocument.end_session_endpoint,
}
this.scheme.options.clientId = configurationDocument.client_id
}
} It gets all the OpenID information from the back-end and puts them in the same configuration document as inherited. It then overrides the options and set the necessary endpoints and client id. (Back-End has to send client id!) Nuxt config: strategies: {
oidc: {
scheme: '~/schemes/customScheme',
scope: ['openid', 'profile', 'offline_access'],
codeChallengeMethod: 'S256'
},
} Feel free to shoot if you need any explaining. |
Beta Was this translation helpful? Give feedback.
-
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. nuxt/nuxt#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].
Beta Was this translation helpful? Give feedback.
All reactions