-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AD FS namespace OIDC bug fix #19460
Merged
hellobontempo
merged 10 commits into
main
from
ui/VAULT-13897/regression-namespace-oidc
Mar 23, 2023
Merged
AD FS namespace OIDC bug fix #19460
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d58eafd
the fix
Monkeychip a944224
changelog
Monkeychip 273a387
clair fix
Monkeychip 00e1518
add test
hellobontempo b7ee87e
update changelog
hellobontempo 34ec5b2
clarify comment
hellobontempo bea042f
remove state from paramsFor completely, update tests
hellobontempo 47d515d
Revert "remove state from paramsFor completely, update tests"
hellobontempo 69a7cbe
add tests with skips until not flaky
hellobontempo eb5367a
Merge branch 'main' into ui/VAULT-13897/regression-namespace-oidc
hellobontempo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for all these test updates 🎉 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import { module, test } from 'qunit'; | ||
import { module, skip, test } from 'qunit'; | ||
import { setupTest } from 'ember-qunit'; | ||
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.windowStub = sinon.stub(window.opener, 'postMessage'); | ||
this.state = 'st_yOarDguU848w5YZuotLs'; | ||
this.path = 'oidc'; | ||
this.code = 'lTazRXEwKfyGKBUCo5TyLJzdIt39YniBJOXPABiRMkL0T'; | ||
this.state = (ns) => { | ||
return ns ? 'st_91ji6vR2sQ2zBiZSQkqJ' + `,ns=${ns}` : 'st_91ji6vR2sQ2zBiZSQkqJ'; | ||
this.route.paramsFor = (path) => { | ||
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 () { | ||
this.windowStub.restore(); | ||
window.opener = this.originalOpener; | ||
this.callbackUrlQueryParams(''); | ||
}); | ||
|
||
test('it calls route', function (assert) { | ||
assert.ok(this.route); | ||
}); | ||
|
||
test('it uses namespace param from state not namespaceQueryParam from cluster with default 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) { | ||
skip('it uses namespace param from state instead of cluster, with custom oidc path', function (assert) { | ||
this.routeName = 'vault.cluster.oidc-callback'; | ||
this.callbackUrlQueryParams(encodeURIComponent(`${this.state},ns=test-ns`)); | ||
this.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' }; | ||
return { | ||
auth_path: 'oidc-dev', | ||
state: this.state('admin/child-ns'), | ||
code: this.code, | ||
}; | ||
}; | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
code: this.code, | ||
path: 'oidc-dev', | ||
namespace: 'admin/child-ns', | ||
state: this.state(), | ||
namespace: 'test-ns', | ||
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.callbackUrlQueryParams(encodeURIComponent(this.state)); | ||
this.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: 'admin' }; | ||
return { | ||
auth_path: this.path, | ||
state: this.state(), | ||
code: this.code, | ||
}; | ||
}; | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
code: this.code, | ||
path: this.path, | ||
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) { | ||
this.routeName = 'vault.cluster.oidc-callback'; | ||
this.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: '' }; | ||
return { | ||
auth_path: this.path, | ||
state: this.state('ns1'), | ||
code: this.code, | ||
}; | ||
}; | ||
skip('it correctly parses encoded, nested ns param from state', function (assert) { | ||
this.callbackUrlQueryParams(encodeURIComponent(`${this.state},ns=parent-ns/child-ns`)); | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
code: this.code, | ||
path: this.path, | ||
namespace: 'ns1', | ||
state: this.state(), | ||
namespace: 'parent-ns/child-ns', | ||
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.callbackUrlQueryParams(''); | ||
this.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: '' }; | ||
return { | ||
auth_path: '', | ||
state: '', | ||
code: '', | ||
}; | ||
}; | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
path: '', | ||
state: '', | ||
code: '', | ||
source: 'oidc-callback', | ||
}, | ||
'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.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: '' }; | ||
return { | ||
auth_path: this.path, | ||
}; | ||
}; | ||
this.callbackUrlQueryParams('stateless'); | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
code: '', | ||
code: this.code, | ||
path: 'oidc', | ||
state: '', | ||
source: 'oidc-callback', | ||
}, | ||
'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.callbackUrlQueryParams(''); | ||
this.route.paramsFor = (path) => { | ||
if (path === 'vault.cluster') return { namespaceQueryParam: 'ns1' }; | ||
return { | ||
auth_path: '', | ||
state: '', | ||
code: '', | ||
}; | ||
}; | ||
this.route.afterModel(); | ||
assert.propContains( | ||
assert.propEqual( | ||
this.windowStub.lastCall.args[0], | ||
{ | ||
code: '', | ||
namespace: 'ns1', | ||
path: '', | ||
source: 'oidc-callback', | ||
state: '', | ||
code: '', | ||
}, | ||
'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' } | ||
*/ | ||
Comment on lines
+182
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great comments here thanks! |
||
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' | ||
); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may seem redundant to first get namespace using
paramsFor
and then again usingwindow.location.search
but if the namespace is from the cluster (i.e. HCP namespace flag)window.location.search
is empty and so we have to useparamsFor
to initially assign those variables.