OIDC Auth Bug (#13133)

* fixes issue with oidc auth method when MetaMask chrome extenstion is used

* adds changelog entry

* updates auth-jwt integration tests

* fixes race condition in runCommands ui-panel helper method where running multiple commands would not always result in the same output order
This commit is contained in:
Jordan Reimer 2021-11-15 08:48:11 -07:00 committed by GitHub
parent 8b869dde70
commit a3862bcf97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 8 deletions

3
changelog/13133.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes issue with OIDC auth workflow when using MetaMask Chrome extension
```

View File

@ -89,12 +89,18 @@ export default Component.extend({
// start watching the popup window and the current one
this.watchPopup.perform(oidcWindow);
this.watchCurrent.perform(oidcWindow);
// wait for message posted from popup
const event = yield waitForEvent(thisWindow, 'message');
if (event.origin === thisWindow.origin && event.isTrusted) {
this.exchangeOIDC.perform(event.data, oidcWindow);
} else {
this.handleOIDCError();
// wait for message posted from oidc callback
// see issue https://github.com/hashicorp/vault/issues/12436
// ensure that postMessage event is from expected source
while (true) {
const event = yield waitForEvent(thisWindow, 'message');
if (event.origin !== thisWindow.origin || !event.isTrusted) {
return this.handleOIDCError();
}
if (event.data.source === 'oidc-callback') {
return this.exchangeOIDC.perform(event.data, oidcWindow);
}
// continue to wait for the correct message
}
}),

View File

@ -9,7 +9,8 @@ export default Route.extend({
let { auth_path: path, code, state } = this.paramsFor(this.routeName);
let { namespaceQueryParam: namespace } = this.paramsFor('vault.cluster');
path = window.decodeURIComponent(path);
let queryParams = { namespace, path, code, state };
const source = 'oidc-callback'; // required by event listener in auth-jwt component
let queryParams = { source, namespace, path, code, state };
window.opener.postMessage(queryParams, window.origin);
},
renderTemplate() {

View File

@ -211,7 +211,10 @@ module('Integration | Component | auth jwt', function(hooks) {
await waitUntil(() => {
return this.openSpy.calledOnce;
});
this.window.trigger('message', buildMessage({ data: { state: 'state', foo: 'bar' } }));
this.window.trigger(
'message',
buildMessage({ data: { source: 'oidc-callback', state: 'state', foo: 'bar' } })
);
run.cancelTimers();
assert.equal(this.error, ERROR_MISSING_PARAMS, 'calls onError with params missing error');
});
@ -228,6 +231,7 @@ module('Integration | Component | auth jwt', function(hooks) {
'message',
buildMessage({
data: {
source: 'oidc-callback',
path: 'foo',
state: 'state',
code: 'code',
@ -253,6 +257,7 @@ module('Integration | Component | auth jwt', function(hooks) {
buildMessage({
origin: 'http://hackerz.com',
data: {
source: 'oidc-callback',
path: 'foo',
state: 'state',
code: 'code',
@ -277,6 +282,7 @@ module('Integration | Component | auth jwt', function(hooks) {
buildMessage({
isTrusted: false,
data: {
source: 'oidc-callback',
path: 'foo',
state: 'state',
code: 'code',

View File

@ -1,5 +1,6 @@
import { text, triggerable, clickable, collection, fillable, value, isPresent } from 'ember-cli-page-object';
import { getter } from 'ember-cli-page-object/macros';
import { settled } from '@ember/test-helpers';
import keys from 'vault/lib/keycodes';
@ -45,6 +46,7 @@ export default {
for (let command of toExecute) {
await this.consoleInput(command);
await this.enter();
await settled();
}
},
};

View File

@ -0,0 +1,65 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import EmberObject from '@ember/object';
import Evented from '@ember/object/evented';
import sinon from 'sinon';
import { run } from '@ember/runloop';
const mockWindow = EmberObject.extend(Evented, {
origin: 'http://localhost:4200',
});
module('Unit | Component | auth-jwt', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.component = this.owner.lookup('component:auth-jwt');
this.component.set('window', mockWindow.create());
this.errorSpy = sinon.spy(this.component, 'handleOIDCError');
});
test('it should handle error for cross origin messages while waiting for oidc callback', async function(assert) {
assert.expect(1);
this.component.prepareForOIDC.perform(mockWindow.create());
this.component.window.trigger('message', { origin: 'http://anotherdomain.com', isTrusted: true });
assert.ok(this.errorSpy.calledOnce, 'Error handled from cross origin window message event');
run.cancelTimers();
});
test('it should handle error for untrusted messages while waiting for oidc callback', async function(assert) {
assert.expect(1);
this.component.prepareForOIDC.perform(mockWindow.create());
this.component.window.trigger('message', { origin: 'http://localhost:4200', isTrusted: false });
assert.ok(this.errorSpy.calledOnce, 'Error handled from untrusted window message event');
run.cancelTimers();
});
// test case for https://github.com/hashicorp/vault/issues/12436
test('it should ignore messages sent from outside the app while waiting for oidc callback', async function(assert) {
assert.expect(2);
this.component.prepareForOIDC.perform(mockWindow.create());
const message = {
origin: 'http://localhost:4200',
isTrusted: true,
data: {
namespace: 'foobar',
path: '/foo/bar',
state: 'authorized',
code: 204,
},
};
this.component.window.trigger('message', message);
message.data.source = 'foo-bar';
this.component.window.trigger('message', message);
message.data.source = 'oidc-callback';
this.component.window.trigger('message', message);
assert.ok(this.errorSpy.notCalled, 'Error handler not triggered while waiting for oidc callback message');
assert.equal(
this.component.exchangeOIDC.performCount,
1,
'exchangeOIDC method fires when oidc callback message is received'
);
run.cancelTimers();
});
});