diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index 50b4ab20..931fab90 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -120,7 +120,10 @@ export default class MattermostView extends React.Component { // So this would be emitted again when reloading a webview webview.addEventListener('dom-ready', () => { // webview.openDevTools(); - + // Remove this once https://github.com/electron/electron/issues/14474 is fixed + // - fixes missing cursor bug in electron + webview.blur(); + webview.focus(); if (!this.state.isContextMenuAdded) { contextMenu.setup(webview, { useSpellChecker: this.props.useSpellChecker, diff --git a/src/main.js b/src/main.js index 3284f36c..f1de490d 100644 --- a/src/main.js +++ b/src/main.js @@ -67,6 +67,23 @@ let config = null; let trayIcon = null; let trayImages = null; +// supported custom login paths (oath, saml) +const customLoginRegexPaths = [ + /^\/oauth\/authorize$/i, + /^\/oauth\/deauthorize$/i, + /^\/oauth\/access_token$/i, + /^\/oauth\/[A-Za-z0-9]+\/complete$/i, + /^\/oauth\/[A-Za-z0-9]+\/login$/i, + /^\/oauth\/[A-Za-z0-9]+\/signup$/i, + /^\/api\/v3\/oauth\/[A-Za-z0-9]+\/complete$/i, + /^\/signup\/[A-Za-z0-9]+\/complete$/i, + /^\/login\/[A-Za-z0-9]+\/complete$/i, + /^\/login\/sso\/saml$/i, +]; + +// tracking in progress custom logins +const customLogins = {}; + /** * Main entry point for the application, ensures that everything initializes in the proper order */ @@ -361,26 +378,77 @@ function handleAppWillFinishLaunching() { } function handleAppWebContentsCreated(dc, contents) { + // initialize custom login tracking + customLogins[contents.id] = { + inProgress: false, + startingURL: null, + externalHostname: null, + }; + contents.on('will-attach-webview', (event, webPreferences) => { webPreferences.nodeIntegration = false; webPreferences.contextIsolation = true; }); - contents.on('will-navigate', (event, navigationUrl) => { - const parsedUrl = new URL(navigationUrl); - const trustedURLs = config.teams.map((team) => new URL(team.url)); //eslint-disable-line max-nested-callbacks - let trusted = false; - for (const url of trustedURLs) { - if (parsedUrl.origin === url.origin) { - trusted = true; - break; + contents.on('will-navigate', (event, url) => { + const contentID = event.sender.id; + const parsedUrl = new URL(url); + const urlIsTrusted = isTrustedURL(parsedUrl); + const urlIsTrustedExternalLoginPath = parsedUrl.hostname === customLogins[contentID].externalHostname; + + // don't prevent custom login attempts (oath, saml) + if (!urlIsTrusted && !urlIsTrustedExternalLoginPath) { + event.preventDefault(); + + // reset custom login if in progress + if (customLogins[contentID].inProgress) { + customLogins[contentID].inProgress = false; + customLogins[contentID].externalHostname = null; + + // redirect to starting url if set + if (customLogins[contentID].startingURL) { + event.sender.loadURL(customLogins[contentID].startingURL); + customLogins[contentID].startingURL = null; + } } } + }); - if (!trusted) { - event.preventDefault(); + // handle custom login requests (oath, saml): + // 1. are we navigating to a supported local custom login path from the `/login` page? (did-start-navigation listener) + // - indicate custom login is in progress and store starting point for possible reset + // 2. is a custom login in progress but we don't have the 3rd party hostname yet? (will-redirect listener) + // - store 3rd party hostname to trust subsequent navigation changes within that hostname + // 3. are we finished with the custom login process? (did-start-navigation listener) + // - indicate custom login is NOT in progress and clear any stored 3rd party hostname's + contents.on('did-start-navigation', (event, url) => { + const contentID = event.sender.id; + const parsedUrl = new URL(url); + const urlIsTrusted = isTrustedURL(parsedUrl); + const urlIsCustomLoginPath = isCustomLoginURL(parsedUrl); + const previousPage = event.sender.history[event.sender.history.length - 1]; + + if (urlIsTrusted && urlIsCustomLoginPath && !customLogins[contentID].inProgress && previousPage.endsWith('/login')) { + customLogins[contentID].inProgress = true; + customLogins[contentID].startingURL = event.sender.getURL(); + } else if (urlIsTrusted && customLogins[contentID].inProgress && customLogins[contentID].externalHostname) { + customLogins[contentID].inProgress = false; + customLogins[contentID].externalHostname = null; } }); + + contents.on('will-redirect', (event, url) => { + const contentID = event.sender.id; + const parsedUrl = new URL(url); + const urlIsTrusted = isTrustedURL(parsedUrl); + const previousPage = event.sender.history[event.sender.history.length - 1]; + const previousPageIsTrusted = isTrustedURL(previousPage); + + if (!urlIsTrusted && previousPageIsTrusted && customLogins[contentID].inProgress && !customLogins[contentID].externalHostname) { + customLogins[contentID].externalHostname = parsedUrl.hostname; + } + }); + contents.on('new-window', (event) => { event.preventDefault(); }); @@ -674,6 +742,39 @@ function handleMainWindowWebContentsCrashed() { // helper functions // +function isTrustedURL(url) { + let parsedUrl = url; + if (typeof url === 'string') { + parsedUrl = new URL(url); + } + const trustedURLs = config.teams.map((team) => new URL(team.url)); + + for (const trustedURL of trustedURLs) { + if (parsedUrl.origin === trustedURL.origin) { + return true; + } + } + + return false; +} + +function isCustomLoginURL(url) { + if (!isTrustedURL(url)) { + return false; + } + let parsedUrl = url; + if (typeof url === 'string') { + parsedUrl = new URL(url); + } + const urlPath = parsedUrl.pathname; + for (const regexPath of customLoginRegexPaths) { + if (urlPath.match(regexPath)) { + return true; + } + } + return false; +} + function getTrayImages() { switch (process.platform) { case 'win32':