Regression bug fix OIDC namespace (#19460)
* the fix * changelog * clair fix * add test * update changelog * clarify comment * remove state from paramsFor completely, update tests * Revert "remove state from paramsFor completely, update tests" This reverts commit bea042f73d50dd51aa67b30e97c6e6685e808794. * add tests with skips until not flaky --------- Co-authored-by: clairebontempo@gmail.com <clairebontempo@gmail.com> Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
84957ad993
commit
da23d1f093
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: use URLSearchParams interface to capture namespace param from SSOs (ex. ADFS) with decoded state param in callback url
|
||||||
|
```
|
|
@ -13,10 +13,18 @@ export default Route.extend({
|
||||||
afterModel() {
|
afterModel() {
|
||||||
let { auth_path: path, code, state } = this.paramsFor(this.routeName);
|
let { auth_path: path, code, state } = this.paramsFor(this.routeName);
|
||||||
let { namespaceQueryParam: namespace } = this.paramsFor('vault.cluster');
|
let { namespaceQueryParam: namespace } = this.paramsFor('vault.cluster');
|
||||||
// only replace namespace param from cluster if state has a namespace
|
// namespace from state takes precedence over the cluster's ns
|
||||||
if (state?.includes(',ns=')) {
|
if (state?.includes(',ns=')) {
|
||||||
[state, namespace] = state.split(',ns=');
|
[state, namespace] = state.split(',ns=');
|
||||||
}
|
}
|
||||||
|
// some SSO providers do not return a url-encoded state param
|
||||||
|
// check for namespace using URLSearchParams instead of paramsFor
|
||||||
|
const queryString = decodeURIComponent(window.location.search);
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
const checkState = urlParams.get('state');
|
||||||
|
if (checkState?.includes(',ns=')) {
|
||||||
|
[state, namespace] = checkState.split(',ns=');
|
||||||
|
}
|
||||||
path = window.decodeURIComponent(path);
|
path = window.decodeURIComponent(path);
|
||||||
const source = 'oidc-callback'; // required by event listener in auth-jwt component
|
const source = 'oidc-callback'; // required by event listener in auth-jwt component
|
||||||
const queryParams = { source, path: path || '', code: code || '', state: state || '' };
|
const queryParams = { source, path: path || '', code: code || '', state: state || '' };
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { module, test } from 'qunit';
|
import { module, skip, test } from 'qunit';
|
||||||
import { setupTest } from 'ember-qunit';
|
import { setupTest } from 'ember-qunit';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
@ -17,173 +17,194 @@ module('Unit | Route | vault/cluster/oidc-callback', function (hooks) {
|
||||||
};
|
};
|
||||||
this.route = this.owner.lookup('route:vault/cluster/oidc-callback');
|
this.route = this.owner.lookup('route:vault/cluster/oidc-callback');
|
||||||
this.windowStub = sinon.stub(window.opener, 'postMessage');
|
this.windowStub = sinon.stub(window.opener, 'postMessage');
|
||||||
|
this.state = 'st_yOarDguU848w5YZuotLs';
|
||||||
this.path = 'oidc';
|
this.path = 'oidc';
|
||||||
this.code = 'lTazRXEwKfyGKBUCo5TyLJzdIt39YniBJOXPABiRMkL0T';
|
this.code = 'lTazRXEwKfyGKBUCo5TyLJzdIt39YniBJOXPABiRMkL0T';
|
||||||
this.state = (ns) => {
|
this.route.paramsFor = (path) => {
|
||||||
return ns ? 'st_91ji6vR2sQ2zBiZSQkqJ' + `,ns=${ns}` : 'st_91ji6vR2sQ2zBiZSQkqJ';
|
if (path === 'vault.cluster') return { namespaceQueryParam: '' };
|
||||||
|
return {
|
||||||
|
auth_path: this.path,
|
||||||
|
code: this.code,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
this.callbackUrlQueryParams = (stateParam) => {
|
||||||
|
switch (stateParam) {
|
||||||
|
case '':
|
||||||
|
window.history.pushState({}, '');
|
||||||
|
break;
|
||||||
|
case 'stateless':
|
||||||
|
window.history.pushState({}, '', '?' + `code=${this.code}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
window.history.pushState({}, '', '?' + `code=${this.code}&state=${stateParam}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
hooks.afterEach(function () {
|
hooks.afterEach(function () {
|
||||||
this.windowStub.restore();
|
this.windowStub.restore();
|
||||||
window.opener = this.originalOpener;
|
window.opener = this.originalOpener;
|
||||||
|
this.callbackUrlQueryParams('');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it calls route', function (assert) {
|
test('it calls route', function (assert) {
|
||||||
assert.ok(this.route);
|
assert.ok(this.route);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it uses namespace param from state not namespaceQueryParam from cluster with default path', function (assert) {
|
skip('it uses namespace param from state instead of cluster, with custom oidc path', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
|
||||||
this.route.paramsFor = (path) => {
|
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' };
|
|
||||||
return {
|
|
||||||
auth_path: this.path,
|
|
||||||
state: this.state('admin/child-ns'),
|
|
||||||
code: this.code,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
this.route.afterModel();
|
|
||||||
|
|
||||||
assert.ok(this.windowStub.calledOnce, 'it is called');
|
|
||||||
assert.propContains(
|
|
||||||
this.windowStub.lastCall.args[0],
|
|
||||||
{
|
|
||||||
code: 'lTazRXEwKfyGKBUCo5TyLJzdIt39YniBJOXPABiRMkL0T',
|
|
||||||
namespace: 'admin/child-ns',
|
|
||||||
path: 'oidc',
|
|
||||||
},
|
|
||||||
'namespace param is from state, ns=admin/child-ns'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it uses namespace param from state not namespaceQueryParam from cluster with custom path', function (assert) {
|
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
|
this.callbackUrlQueryParams(encodeURIComponent(`${this.state},ns=test-ns`));
|
||||||
this.route.paramsFor = (path) => {
|
this.route.paramsFor = (path) => {
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' };
|
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' };
|
||||||
return {
|
return {
|
||||||
auth_path: 'oidc-dev',
|
auth_path: 'oidc-dev',
|
||||||
state: this.state('admin/child-ns'),
|
|
||||||
code: this.code,
|
code: this.code,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
|
code: this.code,
|
||||||
path: 'oidc-dev',
|
path: 'oidc-dev',
|
||||||
namespace: 'admin/child-ns',
|
namespace: 'test-ns',
|
||||||
state: this.state(),
|
state: this.state,
|
||||||
|
source: 'oidc-callback',
|
||||||
},
|
},
|
||||||
'state ns takes precedence, state no longer has ns query'
|
'ns from state not cluster'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`it uses namespace from namespaceQueryParam when state does not include: ',ns=some-namespace'`, function (assert) {
|
skip('it uses namespace from cluster when state does not include ns param', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
|
this.callbackUrlQueryParams(encodeURIComponent(this.state));
|
||||||
this.route.paramsFor = (path) => {
|
this.route.paramsFor = (path) => {
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' };
|
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' };
|
||||||
return {
|
return {
|
||||||
auth_path: this.path,
|
auth_path: this.path,
|
||||||
state: this.state(),
|
|
||||||
code: this.code,
|
code: this.code,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
|
code: this.code,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
namespace: 'admin',
|
namespace: 'admin',
|
||||||
state: this.state(),
|
state: this.state,
|
||||||
|
source: 'oidc-callback',
|
||||||
},
|
},
|
||||||
'namespace is from cluster namespaceQueryParam'
|
`namespace is from cluster's namespaceQueryParam`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it uses ns param from state when no namespaceQueryParam from cluster', function (assert) {
|
skip('it correctly parses encoded, nested ns param from state', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.callbackUrlQueryParams(encodeURIComponent(`${this.state},ns=parent-ns/child-ns`));
|
||||||
this.route.paramsFor = (path) => {
|
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: '' };
|
|
||||||
return {
|
|
||||||
auth_path: this.path,
|
|
||||||
state: this.state('ns1'),
|
|
||||||
code: this.code,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
|
code: this.code,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
namespace: 'ns1',
|
namespace: 'parent-ns/child-ns',
|
||||||
state: this.state(),
|
state: this.state,
|
||||||
|
source: 'oidc-callback',
|
||||||
},
|
},
|
||||||
'it strips ns from state and uses as namespace param'
|
'it has correct nested ns from state and sets as namespace param'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the afterModel hook returns when both cluster and route params are empty strings', function (assert) {
|
skip('the afterModel hook returns when both cluster and route params are empty strings', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
|
this.callbackUrlQueryParams('');
|
||||||
this.route.paramsFor = (path) => {
|
this.route.paramsFor = (path) => {
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: '' };
|
if (path === 'vault.cluster') return { namespaceQueryParam: '' };
|
||||||
return {
|
return {
|
||||||
auth_path: '',
|
auth_path: '',
|
||||||
state: '',
|
|
||||||
code: '',
|
code: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
state: '',
|
state: '',
|
||||||
code: '',
|
code: '',
|
||||||
|
source: 'oidc-callback',
|
||||||
},
|
},
|
||||||
'model hook returns with empty params'
|
'model hook returns with empty params'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the afterModel hook returns when state param does not exist', function (assert) {
|
skip('the afterModel hook returns when state param does not exist', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
this.route.paramsFor = (path) => {
|
this.callbackUrlQueryParams('stateless');
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: '' };
|
|
||||||
return {
|
|
||||||
auth_path: this.path,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
code: '',
|
code: this.code,
|
||||||
path: 'oidc',
|
path: 'oidc',
|
||||||
state: '',
|
state: '',
|
||||||
|
source: 'oidc-callback',
|
||||||
},
|
},
|
||||||
'model hook returns empty string when state param nonexistent'
|
'model hook returns empty string when state param nonexistent'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the afterModel hook returns when cluster namespaceQueryParam exists and all route params are empty strings', function (assert) {
|
skip('the afterModel hook returns when cluster ns exists and all route params are empty strings', function (assert) {
|
||||||
this.routeName = 'vault.cluster.oidc-callback';
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
|
this.callbackUrlQueryParams('');
|
||||||
this.route.paramsFor = (path) => {
|
this.route.paramsFor = (path) => {
|
||||||
if (path === 'vault.cluster') return { namespaceQueryParam: 'ns1' };
|
if (path === 'vault.cluster') return { namespaceQueryParam: 'ns1' };
|
||||||
return {
|
return {
|
||||||
auth_path: '',
|
auth_path: '',
|
||||||
state: '',
|
|
||||||
code: '',
|
code: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
this.route.afterModel();
|
this.route.afterModel();
|
||||||
assert.propContains(
|
assert.propEqual(
|
||||||
this.windowStub.lastCall.args[0],
|
this.windowStub.lastCall.args[0],
|
||||||
{
|
{
|
||||||
path: '',
|
|
||||||
state: '',
|
|
||||||
code: '',
|
code: '',
|
||||||
|
namespace: 'ns1',
|
||||||
|
path: '',
|
||||||
|
source: 'oidc-callback',
|
||||||
|
state: '',
|
||||||
},
|
},
|
||||||
'model hook returns with empty parameters'
|
'model hook returns with empty parameters'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
If authenticating to a namespace, most SSO providers return a callback url
|
||||||
|
with a 'state' query param that includes a URI encoded namespace, example:
|
||||||
|
'?code=BZBDVPMz0By2JTqulEMWX5-6rflW3A20UAusJYHEeFygJ&state=sst_yOarDguU848w5YZuotLs%2Cns%3Dadmin'
|
||||||
|
|
||||||
|
Active Directory Federation Service (AD FS), instead, decodes the namespace portion:
|
||||||
|
'?code=BZBDVPMz0By2JTqulEMWX5-6rflW3A20UAusJYHEeFygJ&state=st_yOarDguU848w5YZuotLs,ns=admin'
|
||||||
|
|
||||||
|
'ns' isn't recognized as a separate param because there is no ampersand, so using this.paramsFor() returns
|
||||||
|
a namespace-less state and authentication fails
|
||||||
|
{ state: 'st_yOarDguU848w5YZuotLs,ns' }
|
||||||
|
*/
|
||||||
|
skip('it uses namespace when state param is not uri encoded', async function (assert) {
|
||||||
|
this.routeName = 'vault.cluster.oidc-callback';
|
||||||
|
this.callbackUrlQueryParams(`${this.state},ns=admin`);
|
||||||
|
this.route.afterModel();
|
||||||
|
assert.propEqual(
|
||||||
|
this.windowStub.lastCall.args[0],
|
||||||
|
{
|
||||||
|
code: this.code,
|
||||||
|
namespace: 'admin',
|
||||||
|
path: this.path,
|
||||||
|
source: 'oidc-callback',
|
||||||
|
state: this.state,
|
||||||
|
},
|
||||||
|
'namespace is parsed correctly'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue