mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	UI/fix safari oidc login (#11884)
* use window.postMessage instead of localStorage on oidc callback
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/11884.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/11884.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:bug | ||||
| ui: fix oidc login with Safari | ||||
| ``` | ||||
| @@ -83,15 +83,19 @@ export default Component.extend({ | ||||
|   }, | ||||
|  | ||||
|   prepareForOIDC: task(function*(oidcWindow) { | ||||
|     const thisWindow = this.getWindow(); | ||||
|     // show the loading animation in the parent | ||||
|     this.onLoading(true); | ||||
|     // start watching the popup window and the current one | ||||
|     this.watchPopup.perform(oidcWindow); | ||||
|     this.watchCurrent.perform(oidcWindow); | ||||
|     // and then wait for storage event to be fired from the popup | ||||
|     // window setting a value in localStorage when the callback route is loaded | ||||
|     let storageEvent = yield waitForEvent(this.getWindow(), 'storage'); | ||||
|     this.exchangeOIDC.perform(storageEvent, 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(); | ||||
|     } | ||||
|   }), | ||||
|  | ||||
|   watchPopup: task(function*(oidcWindow) { | ||||
| @@ -104,6 +108,7 @@ export default Component.extend({ | ||||
|   }), | ||||
|  | ||||
|   watchCurrent: task(function*(oidcWindow) { | ||||
|     // when user is about to change pages, close the popup window | ||||
|     yield waitForEvent(this.getWindow(), 'beforeunload'); | ||||
|     oidcWindow.close(); | ||||
|   }), | ||||
| @@ -114,20 +119,13 @@ export default Component.extend({ | ||||
|     oidcWindow.close(); | ||||
|   }, | ||||
|  | ||||
|   exchangeOIDC: task(function*(event, oidcWindow) { | ||||
|     // in non-incognito mode we need to use a timeout because it takes time before oidcState is written to local storage. | ||||
|     let oidcState = Ember.testing | ||||
|       ? event.storageArea.getItem('oidcState') | ||||
|       : yield timeout(1000).then(() => event.storageArea.getItem('oidcState')); | ||||
|  | ||||
|   exchangeOIDC: task(function*(oidcState, oidcWindow) { | ||||
|     if (oidcState === null || oidcState === undefined) { | ||||
|       return; | ||||
|     } | ||||
|     this.onLoading(true); | ||||
|     // get the info from the event fired by the other window and | ||||
|     // then remove it from localStorage | ||||
|     let { namespace, path, state, code } = JSON.parse(oidcState); | ||||
|     this.getWindow().localStorage.removeItem('oidcState'); | ||||
|  | ||||
|     let { namespace, path, state, code } = oidcState; | ||||
|  | ||||
|     // The namespace can be either be passed as a query paramter, or be embedded | ||||
|     // in the state param in the format `<state_id>,ns=<namespace>`. So if | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export default Route.extend({ | ||||
|     let { namespaceQueryParam: namespace } = this.paramsFor('vault.cluster'); | ||||
|     path = window.decodeURIComponent(path); | ||||
|     let queryParams = { namespace, path, code, state }; | ||||
|     window.localStorage.setItem('oidcState', JSON.stringify(queryParams)); | ||||
|     window.opener.postMessage(queryParams, window.origin); | ||||
|   }, | ||||
|   renderTemplate() { | ||||
|     this.render(this.templateName, { | ||||
|   | ||||
| @@ -55,7 +55,7 @@ | ||||
|       @roleName={{this.roleName}} | ||||
|       @selectedAuthType={{this.selectedAuthBackend.type}} | ||||
|       @selectedAuthPath={{or this.customPath this.selectedAuthBackend.id}} | ||||
|       @disabled={{authenticate.isRunning}} | ||||
|       @disabled={{or authenticate.isRunning this.isLoading}} | ||||
|     > | ||||
|       <AuthFormOptions | ||||
|         @customPath={{this.customPath}} | ||||
|   | ||||
| @@ -15,6 +15,12 @@ import { ERROR_WINDOW_CLOSED, ERROR_MISSING_PARAMS, ERROR_JWT_LOGIN } from 'vaul | ||||
|  | ||||
| const component = create(form); | ||||
| const windows = []; | ||||
| const buildMessage = opts => ({ | ||||
|   isTrusted: true, | ||||
|   origin: 'https://my-vault.com', | ||||
|   data: {}, | ||||
|   ...opts, | ||||
| }); | ||||
| const fakeWindow = EmberObject.extend(Evented, { | ||||
|   init() { | ||||
|     this._super(...arguments); | ||||
| @@ -29,12 +35,7 @@ const fakeWindow = EmberObject.extend(Evented, { | ||||
|       width: 500, | ||||
|     }; | ||||
|   }), | ||||
|   localStorage: computed(function() { | ||||
|     return { | ||||
|       getItem: sinon.stub(), | ||||
|       removeItem: sinon.stub(), | ||||
|     }; | ||||
|   }), | ||||
|   origin: 'https://my-vault.com', | ||||
|   closed: false, | ||||
| }); | ||||
|  | ||||
| @@ -70,7 +71,6 @@ const renderIt = async (context, path = 'jwt') => { | ||||
|   context.set('handler', sinon.spy(handler)); | ||||
|   context.set('roleName', ''); | ||||
|   context.set('selectedAuthPath', path); | ||||
|  | ||||
|   await render(hbs` | ||||
|     <AuthJwt | ||||
|       @window={{window}} | ||||
| @@ -135,7 +135,7 @@ module('Integration | Component | auth jwt', function(hooks) { | ||||
|     assert.equal(component.yieldContent, 'Hello!', 'yields properly'); | ||||
|   }); | ||||
|  | ||||
|   test('jwt: it renders', async function(assert) { | ||||
|   test('jwt: it renders and makes auth_url requests', async function(assert) { | ||||
|     await renderIt(this); | ||||
|     await settled(); | ||||
|     assert.ok(component.jwtPresent, 'renders jwt field'); | ||||
| @@ -203,7 +203,7 @@ module('Integration | Component | auth jwt', function(hooks) { | ||||
|     assert.equal(this.error, ERROR_WINDOW_CLOSED, 'calls onError with error string'); | ||||
|   }); | ||||
|  | ||||
|   test('oidc: storage event fires without state key', async function(assert) { | ||||
|   test('oidc: shows error when message posted with state key, wrong params', async function(assert) { | ||||
|     await renderIt(this); | ||||
|     this.set('selectedAuthPath', 'foo'); | ||||
|     await component.role('test'); | ||||
| @@ -211,26 +211,8 @@ module('Integration | Component | auth jwt', function(hooks) { | ||||
|     await waitUntil(() => { | ||||
|       return this.openSpy.calledOnce; | ||||
|     }); | ||||
|     this.window.localStorage.getItem.returns(null); | ||||
|     this.window.trigger('storage', { storageArea: this.window.localStorage }); | ||||
|     this.window.trigger('message', buildMessage({ data: { state: 'state', foo: 'bar' } })); | ||||
|     run.cancelTimers(); | ||||
|     assert.ok(this.window.localStorage.getItem.calledOnce, 'calls getItem'); | ||||
|     assert.notOk(this.window.localStorage.removeItem.called, 'never calls removeItem'); | ||||
|   }); | ||||
|  | ||||
|   test('oidc: storage event fires with state key, wrong params', async function(assert) { | ||||
|     await renderIt(this); | ||||
|     this.set('selectedAuthPath', 'foo'); | ||||
|     await component.role('test'); | ||||
|     component.login(); | ||||
|     await waitUntil(() => { | ||||
|       return this.openSpy.calledOnce; | ||||
|     }); | ||||
|     this.window.localStorage.getItem.returns(JSON.stringify({})); | ||||
|     this.window.trigger('storage', { storageArea: this.window.localStorage }); | ||||
|     run.cancelTimers(); | ||||
|     assert.ok(this.window.localStorage.getItem.calledOnce, 'calls getItem'); | ||||
|     assert.ok(this.window.localStorage.removeItem.calledOnce, 'calls removeItem'); | ||||
|     assert.equal(this.error, ERROR_MISSING_PARAMS, 'calls onError with params missing error'); | ||||
|   }); | ||||
|  | ||||
| @@ -242,17 +224,67 @@ module('Integration | Component | auth jwt', function(hooks) { | ||||
|     await waitUntil(() => { | ||||
|       return this.openSpy.calledOnce; | ||||
|     }); | ||||
|     this.window.localStorage.getItem.returns( | ||||
|       JSON.stringify({ | ||||
|         path: 'foo', | ||||
|         state: 'state', | ||||
|         code: 'code', | ||||
|     this.window.trigger( | ||||
|       'message', | ||||
|       buildMessage({ | ||||
|         data: { | ||||
|           path: 'foo', | ||||
|           state: 'state', | ||||
|           code: 'code', | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|     this.window.trigger('storage', { storageArea: this.window.localStorage }); | ||||
|     await settled(); | ||||
|     assert.equal(this.selectedAuth, 'token', 'calls onSelectedAuth with token'); | ||||
|     assert.equal(this.token, 'token', 'calls onToken with token'); | ||||
|     assert.ok(this.handler.calledOnce, 'calls the onSubmit handler'); | ||||
|   }); | ||||
|  | ||||
|   test('oidc: fails silently when event origin does not match window origin', async function(assert) { | ||||
|     await renderIt(this); | ||||
|     this.set('selectedAuthPath', 'foo'); | ||||
|     await component.role('test'); | ||||
|     component.login(); | ||||
|     await waitUntil(() => { | ||||
|       return this.openSpy.calledOnce; | ||||
|     }); | ||||
|     this.window.trigger( | ||||
|       'message', | ||||
|       buildMessage({ | ||||
|         origin: 'http://hackerz.com', | ||||
|         data: { | ||||
|           path: 'foo', | ||||
|           state: 'state', | ||||
|           code: 'code', | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|     run.cancelTimers(); | ||||
|     await settled(); | ||||
|     assert.notOk(this.handler.called, 'should not call the submit handler'); | ||||
|   }); | ||||
|  | ||||
|   test('oidc: fails silently when event is not trusted', async function(assert) { | ||||
|     await renderIt(this); | ||||
|     this.set('selectedAuthPath', 'foo'); | ||||
|     await component.role('test'); | ||||
|     component.login(); | ||||
|     await waitUntil(() => { | ||||
|       return this.openSpy.calledOnce; | ||||
|     }); | ||||
|     this.window.trigger( | ||||
|       'message', | ||||
|       buildMessage({ | ||||
|         isTrusted: false, | ||||
|         data: { | ||||
|           path: 'foo', | ||||
|           state: 'state', | ||||
|           code: 'code', | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|     run.cancelTimers(); | ||||
|     await settled(); | ||||
|     assert.notOk(this.handler.called, 'should not call the submit handler'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw