fix(editor): Use BroadcastChannel instead of window.opener for OAuth callback window (#9779)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-06-17 13:41:49 +02:00 committed by GitHub
parent a1046607bf
commit 87cb199745
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 68 additions and 12 deletions

View file

@ -0,0 +1,46 @@
import { CredentialsPage, CredentialsModal } from '../pages';
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('Credentials', () => {
it('create and connect with Google OAuth2', () => {
// Open credentials page
cy.visit(credentialsPage.url, {
onBeforeLoad(win) {
cy.stub(win, 'open').as('windowOpen');
},
});
// Add a new Google OAuth2 credential
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Google OAuth2 API').click();
credentialsModal.getters.newCredentialTypeButton().click();
// Fill in the key/secret and save
credentialsModal.actions.fillField('clientId', 'test-key');
credentialsModal.actions.fillField('clientSecret', 'test-secret');
credentialsModal.actions.save();
// Connect to Google
credentialsModal.getters.oauthConnectButton().click();
cy.get('@windowOpen').should(
'have.been.calledOnceWith',
Cypress.sinon.match(
'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&client_id=test-key&redirect_uri=http%3A%2F%2Flocalhost%3A5678%2Frest%2Foauth2-credential%2Fcallback&response_type=code',
),
'OAuth Authorization',
'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700',
);
// Emulate successful save using BroadcastChannel
cy.window().then(() => {
const channel = new BroadcastChannel('oauth-callback');
channel.postMessage('success');
});
// Check that the credential was saved and connected successfully
credentialsModal.getters.saveButton().should('contain.text', 'Saved');
credentialsModal.getters.oauthConnectSuccessBanner().should('be.visible');
});
});

View file

@ -9,15 +9,16 @@ export class CredentialsModal extends BasePage {
newCredentialTypeOption: (credentialType: string) => newCredentialTypeOption: (credentialType: string) =>
cy.getByTestId('new-credential-type-select-option').contains(credentialType), cy.getByTestId('new-credential-type-select-option').contains(credentialType),
newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'), newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'),
connectionParameters: () => cy.getByTestId('credential-connection-parameter'),
connectionParameter: (fieldName: string) => connectionParameter: (fieldName: string) =>
this.getters.connectionParameters().find(`:contains('${fieldName}') .n8n-input input`), this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`),
name: () => cy.getByTestId('credential-name'), name: () => cy.getByTestId('credential-name'),
nameInput: () => cy.getByTestId('credential-name').find('input'), nameInput: () => cy.getByTestId('credential-name').find('input'),
// Saving of the credentials takes a while on the CI so we need to increase the timeout // Saving of the credentials takes a while on the CI so we need to increase the timeout
saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }), saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }),
deleteButton: () => cy.getByTestId('credential-delete-button'), deleteButton: () => cy.getByTestId('credential-delete-button'),
closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(), closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(),
oauthConnectButton: () => cy.getByTestId('oauth-connect-button'),
oauthConnectSuccessBanner: () => cy.getByTestId('oauth-connect-success-banner'),
credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'), credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'),
credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'), credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'),
credentialAuthTypeRadioButtons: () => credentialAuthTypeRadioButtons: () =>
@ -69,6 +70,13 @@ export class CredentialsModal extends BasePage {
this.getters.closeButton().click(); this.getters.closeButton().click();
} }
}, },
fillField: (fieldName: string, value: string) => {
this.getters
.credentialInputs()
.getByTestId(`parameter-input-${fieldName}`)
.find('input')
.type(value);
},
createNewCredential: (type: string, closeModal = true) => { createNewCredential: (type: string, closeModal = true) => {
this.getters.newCredentialModal().should('be.visible'); this.getters.newCredentialModal().should('be.visible');
this.getters.newCredentialTypeSelect().should('be.visible'); this.getters.newCredentialTypeSelect().should('be.visible');

View file

@ -1,7 +1,8 @@
<html> <html>
<script> <script>
(function messageParent() { (function messageParent() {
window.opener.postMessage('success', '*'); const broadcastChannel = new BroadcastChannel('oauth-callback');
broadcastChannel.postMessage('success');
})(); })();
</script> </script>

View file

@ -17,7 +17,10 @@
{{/if}} {{/if}}
Failed to connect. The window can be closed now. Failed to connect. The window can be closed now.
<script> <script>
(function messageParent() { window.opener?.postMessage('error', '*'); })(); (function messageParent() {
const broadcastChannel = new BroadcastChannel('oauth-callback');
broadcastChannel.postMessage('error');
})();
</script> </script>
</body> </body>
</html> </html>

View file

@ -39,6 +39,7 @@
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')" :button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
:button-title="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')" :button-title="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')"
@click="$emit('oauth')" @click="$emit('oauth')"
data-test-id="oauth-connect-success-banner"
> >
<template v-if="isGoogleOAuthType" #button> <template v-if="isGoogleOAuthType" #button>
<p <p
@ -118,6 +119,7 @@
" "
:is-google-o-auth-type="isGoogleOAuthType" :is-google-o-auth-type="isGoogleOAuthType"
@click="$emit('oauth')" @click="$emit('oauth')"
data-test-id="oauth-connect-button"
/> />
<n8n-text v-if="isMissingCredentials" color="text-base" size="medium"> <n8n-text v-if="isMissingCredentials" color="text-base" size="medium">

View file

@ -1090,20 +1090,17 @@ export default defineComponent({
const params = const params =
'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700'; 'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700';
const oauthPopup = window.open(url, 'OAuth2 Authorization', params); const oauthPopup = window.open(url, 'OAuth Authorization', params);
this.credentialData = { this.credentialData = {
...this.credentialData, ...this.credentialData,
oauthTokenData: null as unknown as CredentialInformation, oauthTokenData: null as unknown as CredentialInformation,
}; };
const oauthChannel = new BroadcastChannel('oauth-callback');
const receiveMessage = (event: MessageEvent) => { const receiveMessage = (event: MessageEvent) => {
// // TODO: Add check that it came from n8n
// if (event.origin !== 'http://example.org:8080') {
// return;
// }
if (event.data === 'success') { if (event.data === 'success') {
window.removeEventListener('message', receiveMessage, false); oauthChannel.removeEventListener('message', receiveMessage);
// Set some kind of data that status changes. // Set some kind of data that status changes.
// As data does not get displayed directly it does not matter what data. // As data does not get displayed directly it does not matter what data.
@ -1118,8 +1115,7 @@ export default defineComponent({
} }
} }
}; };
oauthChannel.addEventListener('message', receiveMessage);
window.addEventListener('message', receiveMessage, false);
}, },
async onAuthTypeChanged(type: string): Promise<void> { async onAuthTypeChanged(type: string): Promise<void> {
if (!this.activeNodeType?.credentials) { if (!this.activeNodeType?.credentials) {