mirror of https://github.com/rancher/dashboard.git
Add support for Epinio's Dex Auth provider
- Add mechanism for plugins to provide login components - Add epinio login component - Ensure builtin plugins are not removed on log out
This commit is contained in:
parent
59c3cb1a67
commit
23ee18ef37
|
|
@ -264,3 +264,7 @@ epinio:
|
|||
helmChart: Helm Chart
|
||||
warnings:
|
||||
noNamespace: There are no namespaces. Please create one before proceeding
|
||||
model:
|
||||
authConfig:
|
||||
provider:
|
||||
epinio: Dex
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import Login from '@shell/mixins/login';
|
||||
|
||||
export default {
|
||||
mixins: [Login],
|
||||
|
||||
async fetch() {
|
||||
// Fetch the dex redirec url.
|
||||
// The dashboard would normally get this directly from the auth provider, however epinio/dex implement pkce flow
|
||||
// (additional per request validation). To support this the redirect url is per request and thus generated by the BE per login
|
||||
// Unforunatly this process also requires the state to be known up front, so for this provider we create it upfront and pass through
|
||||
// to the auth store
|
||||
const baseNonce = await this.$store.dispatch('auth/createNonce', { provider: 'epinio' });
|
||||
const encodedNonce = await this.$store.dispatch('auth/encodeNonce', baseNonce);
|
||||
const res = await this.$store.dispatch('management/request', { url: `/dex/redirectUrl?state=${ encodedNonce }` });
|
||||
|
||||
const redirectAsUrl = new URL(res.redirectUrl);
|
||||
// We'll need to save this locally for when we exchange the code for a token
|
||||
const pkceCodeVerifier = redirectAsUrl.searchParams.get('code_verifier');
|
||||
|
||||
// The scopes in the redirect url are pulled out and reapplied, however this does not work for dex (a decoded space separator)
|
||||
const scopes = redirectAsUrl.searchParams.get(`scope`); // This decodes it
|
||||
const scopesArray = scopes.split(' ');
|
||||
|
||||
// redirectTo mangles the different scopes together incorrectly, and we're supply our own mangled version anyway, so nuke
|
||||
redirectAsUrl.searchParams.delete('scope');
|
||||
|
||||
this.redirectOpts = {
|
||||
provider: this.name,
|
||||
redirectUrl: redirectAsUrl.toString(),
|
||||
scopes: scopesArray,
|
||||
scopesJoinChart: ' ',
|
||||
|
||||
nonce: baseNonce,
|
||||
persistNonce: {
|
||||
...baseNonce,
|
||||
pkceCodeVerifier
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return { redirectOpts: {} };
|
||||
},
|
||||
|
||||
methods: {
|
||||
login() {
|
||||
this.$store.dispatch('auth/redirectTo', this.redirectOpts);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<button ref="btn" class="btn bg-primary" style="font-size: 18px;" :disabled="$fetchState.pending" @click="login">
|
||||
{{ t('login.loginWithProvider', {provider: displayName}) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue