Merge pull request #13028 from vector-im/t3chguy/poc_riot_desktop_sso_multi_profile

Fix Electron SSO handling to support multiple profiles
pull/13224/head
Michael Telatynski 2020-04-14 17:09:14 +01:00 committed by GitHub
commit 3cbc9997b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 29 deletions

View File

@ -35,7 +35,7 @@ const tray = require('./tray');
const vectorMenu = require('./vectormenu'); const vectorMenu = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler'); const webContentsHandler = require('./webcontents-handler');
const updater = require('./updater'); const updater = require('./updater');
const protocolInit = require('./protocol'); const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol');
const windowStateKeeper = require('electron-window-state'); const windowStateKeeper = require('electron-window-state');
const Store = require('electron-store'); const Store = require('electron-store');
@ -68,7 +68,11 @@ if (argv["help"]) {
app.exit(); app.exit();
} }
if (argv['profile-dir']) { // check if we are passed a profile in the SSO callback url
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
if (userDataPathInProtocol) {
app.setPath('userData', userDataPathInProtocol);
} else if (argv['profile-dir']) {
app.setPath('userData', argv['profile-dir']); app.setPath('userData', argv['profile-dir']);
} else if (argv['profile']) { } else if (argv['profile']) {
app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`); app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`);
@ -243,6 +247,9 @@ ipcMain.on('ipcCall', async function(ev, payload) {
mainWindow.webContents.goForward(); mainWindow.webContents.goForward();
} }
break; break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
default: default:
mainWindow.webContents.send('ipcReply', { mainWindow.webContents.send('ipcReply', {

View File

@ -14,18 +14,68 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
const {app} = require('electron'); const {app} = require("electron");
const path = require("path");
const fs = require("fs");
const PROTOCOL = "riot://";
const SEARCH_PARAM = "riot-desktop-ssoid";
const STORE_FILE_NAME = "sso-sessions.json";
// we getPath userData before electron-main changes it, so this is the default value
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
const processUrl = (url) => { const processUrl = (url) => {
if (!global.mainWindow) return; if (!global.mainWindow) return;
console.log("Handling link: ", url); console.log("Handling link: ", url);
global.mainWindow.loadURL(url.replace("riot://", "vector://")); global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://"));
}; };
module.exports = () => { const readStore = () => {
try {
const s = fs.readFileSync(storePath, { encoding: "utf8" });
const o = JSON.parse(s);
return typeof o === "object" ? o : {};
} catch (e) {
return {};
}
};
const writeStore = (data) => {
fs.writeFileSync(storePath, JSON.stringify(data));
};
module.exports = {
recordSSOSession: (sessionID) => {
const userDataPath = app.getPath('userData');
const store = readStore();
for (const key in store) {
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
if (store[key] === userDataPath) {
delete store[key];
break;
}
}
store[sessionID] = userDataPath;
writeStore(store);
},
getProfileFromDeeplink: (args) => {
// check if we are passed a profile in the SSO callback url
const deeplinkUrl = args.find(arg => arg.startsWith('riot://'));
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
const parsedUrl = new URL(deeplinkUrl);
if (parsedUrl.protocol === 'riot:') {
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
const store = readStore();
console.log("Forwarding to profile: ", store[ssoID]);
return store[ssoID];
}
}
},
protocolInit: () => {
// get all args except `hidden` as it'd mean the app would not get focused // get all args except `hidden` as it'd mean the app would not get focused
// XXX: passing args to protocol handlers only works on Windows, // XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
// so unpackaged deep-linking and --profile passing won't work on Mac/Linux // --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden"); const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden");
if (app.isPackaged) { if (app.isPackaged) {
app.setAsDefaultProtocolClient('riot', process.execPath, args); app.setAsDefaultProtocolClient('riot', process.execPath, args);
@ -44,10 +94,11 @@ module.exports = () => {
// Protocol handler for win32/Linux // Protocol handler for win32/Linux
app.on('second-instance', (ev, commandLine) => { app.on('second-instance', (ev, commandLine) => {
const url = commandLine[commandLine.length - 1]; const url = commandLine[commandLine.length - 1];
if (!url.startsWith("riot://")) return; if (!url.startsWith(PROTOCOL)) return;
processUrl(url); processUrl(url);
}); });
} }
},
}; };

View File

@ -32,6 +32,7 @@ import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner";
import {Categories, Modifiers, registerShortcut} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts"; import {Categories, Modifiers, registerShortcut} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts";
import {Key} from "matrix-react-sdk/src/Keyboard"; import {Key} from "matrix-react-sdk/src/Keyboard";
import React from "react"; import React from "react";
import {randomString} from "matrix-js-sdk/src/randomstring";
const ipcRenderer = window.ipcRenderer; const ipcRenderer = window.ipcRenderer;
const isMac = navigator.platform.toUpperCase().includes('MAC'); const isMac = navigator.platform.toUpperCase().includes('MAC');
@ -250,6 +251,10 @@ export default class ElectronPlatform extends VectorBasePlatform {
description: _td("Previous/next recently visited room or community"), description: _td("Previous/next recently visited room or community"),
}); });
} }
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile
this.ssoID = randomString(32);
this._ipcCall("startSSOFlow", this.ssoID);
} }
async getConfig(): Promise<{}> { async getConfig(): Promise<{}> {
@ -446,6 +451,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { getSSOCallbackUrl(hsUrl: string, isUrl: string): URL {
const url = super.getSSOCallbackUrl(hsUrl, isUrl); const url = super.getSSOCallbackUrl(hsUrl, isUrl);
url.protocol = "riot"; url.protocol = "riot";
url.searchParams.set("riot-desktop-ssoid", this.ssoID);
return url; return url;
} }