2021-10-29 21:54:15 +00:00
|
|
|
import Ember from 'ember';
|
2021-10-13 20:04:39 +00:00
|
|
|
import Route from '@ember/routing/route';
|
|
|
|
import { inject as service } from '@ember/service';
|
|
|
|
|
|
|
|
const AUTH = 'vault.cluster.auth';
|
2021-10-29 21:54:15 +00:00
|
|
|
const PROVIDER = 'vault.cluster.oidc-provider';
|
|
|
|
const NS_PROVIDER = 'vault.cluster.oidc-provider-ns';
|
2021-10-13 20:04:39 +00:00
|
|
|
|
2021-10-29 21:54:15 +00:00
|
|
|
export default class VaultClusterOidcProviderRoute extends Route {
|
2021-10-13 20:04:39 +00:00
|
|
|
@service auth;
|
|
|
|
@service router;
|
|
|
|
|
|
|
|
get win() {
|
|
|
|
return this.window || window;
|
|
|
|
}
|
|
|
|
|
|
|
|
_redirect(url, params) {
|
2021-10-20 14:38:29 +00:00
|
|
|
if (!url) return;
|
2021-10-13 20:04:39 +00:00
|
|
|
let redir = this._buildUrl(url, params);
|
2021-10-29 21:54:15 +00:00
|
|
|
if (Ember.testing) {
|
|
|
|
return redir;
|
|
|
|
}
|
2021-10-13 20:04:39 +00:00
|
|
|
this.win.location.replace(redir);
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeModel(transition) {
|
|
|
|
const currentToken = this.auth.get('currentTokenName');
|
2021-10-29 21:54:15 +00:00
|
|
|
let qp = transition.to.queryParams;
|
|
|
|
// remove redirect_to if carried over from auth
|
|
|
|
qp.redirect_to = null;
|
2021-10-13 20:04:39 +00:00
|
|
|
if (!currentToken && 'none' === qp.prompt?.toLowerCase()) {
|
|
|
|
this._redirect(qp.redirect_uri, {
|
|
|
|
state: qp.state,
|
|
|
|
error: 'login_required',
|
|
|
|
});
|
|
|
|
} else if (!currentToken || 'login' === qp.prompt?.toLowerCase()) {
|
2021-10-29 21:54:15 +00:00
|
|
|
let logout = !!currentToken;
|
2021-10-13 20:04:39 +00:00
|
|
|
if ('login' === qp.prompt?.toLowerCase()) {
|
2021-10-20 14:38:29 +00:00
|
|
|
// need to remove before redirect to avoid infinite loop
|
2021-10-13 20:04:39 +00:00
|
|
|
qp.prompt = null;
|
|
|
|
}
|
2021-10-29 21:54:15 +00:00
|
|
|
return this._redirectToAuth({
|
|
|
|
...transition.to.params,
|
|
|
|
qp,
|
|
|
|
logout,
|
|
|
|
});
|
2021-10-13 20:04:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-29 21:54:15 +00:00
|
|
|
_redirectToAuth({ provider_name, namespace = null, qp, logout = false }) {
|
2021-10-13 20:04:39 +00:00
|
|
|
let { cluster_name } = this.paramsFor('vault.cluster');
|
2021-10-29 21:54:15 +00:00
|
|
|
let url = namespace
|
|
|
|
? this.router.urlFor(NS_PROVIDER, cluster_name, namespace, provider_name, { queryParams: qp })
|
|
|
|
: this.router.urlFor(PROVIDER, cluster_name, provider_name, { queryParams: qp });
|
2021-10-20 14:38:29 +00:00
|
|
|
// This is terrible, I'm sorry
|
|
|
|
// Need to do this because transitionTo (as used in auth-form) expects url without
|
|
|
|
// rootURL /ui/ at the beginning, but urlFor builds it in. We can't use currentRoute
|
|
|
|
// because it hasn't transitioned yet
|
|
|
|
url = url.replace(/^(\/?ui)/, '');
|
2021-10-13 20:04:39 +00:00
|
|
|
if (logout) {
|
|
|
|
this.auth.deleteCurrentToken();
|
|
|
|
}
|
2021-10-29 21:54:15 +00:00
|
|
|
// o param can be anything, as long as it's present the auth page will change
|
|
|
|
let queryParams = {
|
|
|
|
redirect_to: url,
|
|
|
|
o: provider_name,
|
|
|
|
};
|
|
|
|
if (namespace) {
|
|
|
|
queryParams.namespace = namespace;
|
|
|
|
}
|
|
|
|
return this.transitionTo(AUTH, cluster_name, { queryParams });
|
2021-10-13 20:04:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_buildUrl(urlString, params) {
|
|
|
|
try {
|
|
|
|
let url = new URL(urlString);
|
|
|
|
Object.keys(params).forEach(key => {
|
|
|
|
if (params[key]) {
|
|
|
|
url.searchParams.append(key, params[key]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return url;
|
|
|
|
} catch (e) {
|
|
|
|
console.debug('DEBUG: parsing url failed for', urlString);
|
|
|
|
throw new Error('Invalid URL');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleSuccess(response, baseUrl, state) {
|
|
|
|
const { code } = response;
|
|
|
|
let redirectUrl = this._buildUrl(baseUrl, { code, state });
|
2021-10-29 21:54:15 +00:00
|
|
|
if (Ember.testing) {
|
|
|
|
return { redirectUrl };
|
|
|
|
}
|
2021-10-13 20:04:39 +00:00
|
|
|
this.win.location.replace(redirectUrl);
|
|
|
|
}
|
|
|
|
_handleError(errorResp, baseUrl) {
|
|
|
|
let redirectUrl = this._buildUrl(baseUrl, { ...errorResp });
|
2021-10-29 21:54:15 +00:00
|
|
|
if (Ember.testing) {
|
|
|
|
return { redirectUrl };
|
|
|
|
}
|
2021-10-13 20:04:39 +00:00
|
|
|
this.win.location.replace(redirectUrl);
|
|
|
|
}
|
|
|
|
|
2021-10-29 21:54:15 +00:00
|
|
|
/**
|
|
|
|
* Method for getting the parameters from the route. Allows for namespace to be defined on extended route oidc-provider-ns
|
|
|
|
* @param {object} params object passed into the model method
|
|
|
|
* @returns object with provider_name (string), qp (object of query params), decodedRedirect (string, FQDN)
|
|
|
|
*/
|
|
|
|
_getInfoFromParams(params) {
|
|
|
|
let { provider_name, namespace, ...qp } = params;
|
2021-10-13 20:04:39 +00:00
|
|
|
let decodedRedirect = decodeURI(qp.redirect_uri);
|
2021-10-29 21:54:15 +00:00
|
|
|
return {
|
|
|
|
provider_name,
|
|
|
|
qp,
|
|
|
|
decodedRedirect,
|
|
|
|
namespace,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async model(params) {
|
|
|
|
let modelInfo = this._getInfoFromParams(params);
|
|
|
|
let { qp, decodedRedirect, ...routeParams } = modelInfo;
|
2021-10-20 14:38:29 +00:00
|
|
|
let endpoint = this._buildUrl(
|
2021-10-29 21:54:15 +00:00
|
|
|
`${this.win.origin}/v1/identity/oidc/provider/${routeParams.provider_name}/authorize`,
|
2021-10-20 14:38:29 +00:00
|
|
|
qp
|
|
|
|
);
|
2021-10-29 21:54:15 +00:00
|
|
|
if (!qp.redirect_uri) {
|
|
|
|
throw new Error('Missing required query params');
|
|
|
|
}
|
2021-10-13 20:04:39 +00:00
|
|
|
try {
|
2021-10-29 21:54:15 +00:00
|
|
|
const response = await this.auth.ajax(endpoint, 'GET', { namespace: routeParams.namespace });
|
2021-10-13 20:04:39 +00:00
|
|
|
if ('consent' === qp.prompt?.toLowerCase()) {
|
|
|
|
return {
|
|
|
|
consent: {
|
|
|
|
code: response.code,
|
|
|
|
redirect: decodedRedirect,
|
|
|
|
state: qp.state,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2021-10-29 21:54:15 +00:00
|
|
|
return this._handleSuccess(response, decodedRedirect, qp.state);
|
2021-10-13 20:04:39 +00:00
|
|
|
} catch (errorRes) {
|
|
|
|
let resp = await errorRes.json();
|
|
|
|
let code = resp.error;
|
2021-10-20 14:38:29 +00:00
|
|
|
if (code === 'max_age_violation' || resp?.errors?.includes('permission denied')) {
|
2021-10-29 21:54:15 +00:00
|
|
|
this._redirectToAuth({ ...routeParams, qp, logout: true });
|
2021-10-13 20:04:39 +00:00
|
|
|
} else if (code === 'invalid_redirect_uri') {
|
|
|
|
return {
|
|
|
|
error: {
|
|
|
|
title: 'Redirect URI mismatch',
|
|
|
|
message:
|
|
|
|
'The provided redirect_uri is not in the list of allowed redirect URIs. Please make sure you are sending a valid redirect URI from your application.',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else if (code === 'invalid_client_id') {
|
|
|
|
return {
|
|
|
|
error: {
|
|
|
|
title: 'Invalid client ID',
|
|
|
|
message: 'Your client ID is invalid. Please update your configuration and try again.',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
2021-10-29 21:54:15 +00:00
|
|
|
return this._handleError(resp, decodedRedirect);
|
2021-10-13 20:04:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|