diff --git a/pkg/epinio/l10n/en-us.yaml b/pkg/epinio/l10n/en-us.yaml index 6ca5879de5..ba952e2bd4 100644 --- a/pkg/epinio/l10n/en-us.yaml +++ b/pkg/epinio/l10n/en-us.yaml @@ -264,3 +264,7 @@ epinio: helmChart: Helm Chart warnings: noNamespace: There are no namespaces. Please create one before proceeding +model: + authConfig: + provider: + epinio: Dex diff --git a/pkg/epinio/login/epinio.vue b/pkg/epinio/login/epinio.vue new file mode 100644 index 0000000000..8824e16a36 --- /dev/null +++ b/pkg/epinio/login/epinio.vue @@ -0,0 +1,60 @@ + + + diff --git a/shell/pages/auth/login.vue b/shell/pages/auth/login.vue index 1f2eec00c4..094d4196da 100644 --- a/shell/pages/auth/login.vue +++ b/shell/pages/auth/login.vue @@ -14,7 +14,6 @@ import Password from '@shell/components/form/Password'; import { sortBy } from '@shell/utils/sort'; import { configType } from '@shell/models/management.cattle.io.authconfig'; import { mapGetters } from 'vuex'; -import { importLogin } from '@shell/utils/dynamic-importer'; import { _ALL_IF_AUTHED, _MULTI } from '@shell/plugins/dashboard-store/actions'; import { MANAGEMENT, NORMAN } from '@shell/config/types'; import { SETTING } from '@shell/config/settings'; @@ -177,7 +176,7 @@ export default { created() { this.providerComponents = this.providers.map((name) => { - return importLogin(configType[name]); + return this.$store.getters['type-map/importLogin'](configType[name] || name); }); }, diff --git a/shell/pages/auth/verify.vue b/shell/pages/auth/verify.vue index 146aeddb85..b4227aa139 100644 --- a/shell/pages/auth/verify.vue +++ b/shell/pages/auth/verify.vue @@ -43,6 +43,12 @@ export default { try { parsed = JSON.parse(base64Decode((stateStr))); } catch (err) { + const out = store.getters['i18n/t'](`login.error`); + + console.error('Failed to parse nonce'); // eslint-disable-line no-console + + redirect(`/auth/login?err=${ escape(out) }`); + return; } diff --git a/shell/pkg/auto-import.js b/shell/pkg/auto-import.js index fba96b6824..b7d462e2ad 100644 --- a/shell/pkg/auto-import.js +++ b/shell/pkg/auto-import.js @@ -1,6 +1,6 @@ const fs = require('fs'); const path = require('path'); -const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'dialog', 'formatters']; +const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'dialog', 'formatters', 'login']; const contextMap = contextFolders.reduce((map, obj) => { map[obj] = true; diff --git a/shell/store/auth.js b/shell/store/auth.js index 802e0068b5..12a6b4ab79 100644 --- a/shell/store/auth.js +++ b/shell/store/auth.js @@ -160,7 +160,10 @@ export const actions = { return findBy(authConfigs, 'id', id); }, - setNonce({ dispatch }, opt) { + /** + * Create the basic json object used for the nonce (this includes the random nonce/state) + */ + createNonce(ctx, opt) { const out = { nonce: randomStr(16), to: 'vue' }; if ( opt.test ) { @@ -171,7 +174,15 @@ export const actions = { out.provider = opt.provider; } - const strung = JSON.stringify(out); + return out; + }, + + /** + * Save nonce details. Information it contains will be used to validate auth requests/responses + * Note - this may be structurally different than the nonce we encode and send + */ + saveNonce(ctx, opt) { + const strung = JSON.stringify(opt); this.$cookies.set(KEY, strung, { path: '/', @@ -182,6 +193,15 @@ export const actions = { return strung; }, + /** + * Convert the nonce into something we can send + */ + encodeNonce(ctx, nonce) { + const stringify = JSON.stringify(nonce); + + return base64Encode(stringify, 'url'); + }, + async redirectTo({ state, commit, dispatch }, opt = {}) { const provider = opt.provider; let redirectUrl = opt.redirectUrl; @@ -200,7 +220,13 @@ export const actions = { returnToUrl = `${ window.location.origin }/verify-auth-azure`; } - const nonce = await dispatch('setNonce', opt); + // The base nonce that will be sent server way + const baseNonce = opt.nonce || await dispatch('createNonce', opt); + + // Save a possibly expanded nonce + await dispatch('saveNonce', opt.persistNonce || baseNonce); + // Convert the base nonce in to something we can transmit + const encodedNonce = await dispatch('encodeNonce', baseNonce); const fromQuery = unescape(parseUrl(redirectUrl).query?.[GITHUB_SCOPE] || ''); const scopes = fromQuery.split(/[, ]+/).filter(x => !!x); @@ -216,8 +242,8 @@ export const actions = { let url = removeParam(redirectUrl, GITHUB_SCOPE); const params = { - [GITHUB_SCOPE]: scopes.join(','), - [GITHUB_NONCE]: base64Encode(nonce, 'url') + [GITHUB_SCOPE]: scopes.join(opt.scopesJoinChart || ','), // Some providers won't accept comma separated scopes + [GITHUB_NONCE]: encodedNonce }; if (!url.includes(GITHUB_REDIRECT)) { @@ -249,9 +275,16 @@ export const actions = { return ERR_NONCE; } + const body = { code }; + + // If the request came with a pkce code ensure we also sent that in the verify + if (parsed.pkceCodeVerifier) { + body.code_verifier = parsed.pkceCodeVerifier; + } + return dispatch('login', { provider, - body: { code } + body }); }, diff --git a/shell/store/type-map.js b/shell/store/type-map.js index 09372a3b64..1e1d2d8155 100644 --- a/shell/store/type-map.js +++ b/shell/store/type-map.js @@ -137,7 +137,7 @@ import { ensureRegex, escapeHtml, escapeRegex, ucFirst, pluralize } from '@shell/utils/string'; import { - importChart, importList, importDetail, importEdit, listProducts, loadProduct, importCustomPromptRemove, resolveList, resolveEdit, resolveWindowComponent, importWindowComponent, resolveChart, resolveDetail, importDialog + importChart, importList, importDetail, importEdit, listProducts, loadProduct, importCustomPromptRemove, resolveList, resolveEdit, resolveWindowComponent, importWindowComponent, importLogin, resolveChart, resolveDetail, importDialog } from '@shell/utils/dynamic-importer'; import { NAME as EXPLORER } from '@shell/config/product/explorer'; @@ -1152,6 +1152,12 @@ export const getters = { }; }, + importLogin(state, getters, rootState) { + return (authType) => { + return loadExtension(rootState, 'login', authType, importLogin); + }; + }, + componentFor(state, getters) { return (type, subType) => { let key = type;