auth: Move social login process to browser.

Moves the social login to browser since there
was no way to verify the authencity of the
auth process for a custom server and to
prevent phishing attacks.

Fixes #849.

Co-authored-by: Kanishk Kakar <kanishk.kakar@gmail.com>
This commit is contained in:
ViPuL
2020-02-25 20:05:27 +05:30
committed by GitHub
parent 0fb610f858
commit 49b29bfed6
10 changed files with 174 additions and 30 deletions

View File

@@ -38,13 +38,20 @@ const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
const singleInstanceLock = app.requestSingleInstanceLock(); const singleInstanceLock = app.requestSingleInstanceLock();
if (singleInstanceLock) { if (singleInstanceLock) {
app.on('second-instance', () => { app.on('second-instance', (event, argv) => {
if (mainWindow) { if (mainWindow) {
if (mainWindow.isMinimized()) { if (mainWindow.isMinimized()) {
mainWindow.restore(); mainWindow.restore();
} }
mainWindow.show(); mainWindow.show();
// URI scheme handler for systems other than macOS.
// For macOS, see 'open-url' event below.
if (process.platform !== 'darwin') {
const deepLinkingUrl = argv.slice(1);
handleDeepLink(deepLinkingUrl[(isDev && process.platform === 'win32') ? 1 : 0]);
}
} }
}); });
} else { } else {
@@ -66,6 +73,11 @@ const toggleApp = (): any => {
} }
}; };
function handleDeepLink(url: string): void {
mainWindow.webContents.focus();
mainWindow.webContents.send('deep-linking-url', url);
}
function createMainWindow(): Electron.BrowserWindow { function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({ const mainWindowState: windowStateKeeper.State = windowStateKeeper({
@@ -145,6 +157,20 @@ function createMainWindow(): Electron.BrowserWindow {
// Decrease load on GPU (experimental) // Decrease load on GPU (experimental)
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
if (process.platform === 'win32' && isDev) {
app.setAsDefaultProtocolClient('zulip', process.execPath, [path.resolve(process.argv[1])]);
} else {
app.setAsDefaultProtocolClient('zulip');
}
// URI scheme handler for macOS
app.on('open-url', (event, url) => {
event.preventDefault();
if (mainWindow) {
mainWindow.webContents.send('deep-linking-url', url);
}
});
// Temporary fix for Electron render colors differently // Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732 // More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb'); app.commandLine.appendSwitch('force-color-profile', 'srgb');

View File

@@ -43,7 +43,8 @@
"node-json-db": "0.9.2", "node-json-db": "0.9.2",
"request": "2.85.0", "request": "2.85.0",
"semver": "5.4.1", "semver": "5.4.1",
"wurl": "2.5.0" "wurl": "2.5.0",
"crypto-random-string": "3.1.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"node-mac-notifier": "1.1.0" "node-mac-notifier": "1.1.0"

View File

@@ -254,6 +254,10 @@ class WebView extends BaseComponent {
this.$el.openDevTools(); this.$el.openDevTools();
} }
loadURL(url: string): void {
this.$el.loadURL(url);
}
back(): void { back(): void {
if (this.$el.canGoBack()) { if (this.$el.canGoBack()) {
this.$el.goBack(); this.$el.goBack();

View File

@@ -21,6 +21,7 @@ import ReconnectUtil = require('./utils/reconnect-util');
import Logger = require('./utils/logger-util'); import Logger = require('./utils/logger-util');
import CommonUtil = require('./utils/common-util'); import CommonUtil = require('./utils/common-util');
import EnterpriseUtil = require('./utils/enterprise-util'); import EnterpriseUtil = require('./utils/enterprise-util');
import AuthUtil = require('./utils/auth-util');
import Messages = require('./../../resources/messages'); import Messages = require('./../../resources/messages');
interface FunctionalTabProps { interface FunctionalTabProps {
@@ -47,6 +48,7 @@ interface SettingsOptions {
autoUpdate: boolean; autoUpdate: boolean;
betaUpdate: boolean; betaUpdate: boolean;
errorReporting: boolean; errorReporting: boolean;
loginInApp: boolean;
customCSS: boolean; customCSS: boolean;
silent: boolean; silent: boolean;
lastActiveTab: number; lastActiveTab: number;
@@ -199,6 +201,7 @@ class ServerManagerView {
autoUpdate: true, autoUpdate: true,
betaUpdate: false, betaUpdate: false,
errorReporting: true, errorReporting: true,
loginInApp: false,
customCSS: false, customCSS: false,
silent: false, silent: false,
lastActiveTab: 0, lastActiveTab: 0,
@@ -808,6 +811,30 @@ class ServerManagerView {
}); });
} }
ipcRenderer.on('deep-linking-url', (event: Event, url: string) => {
if (!ConfigUtil.getConfigItem('desktopOtp')) {
return;
}
const urlObject = new URL(decodeURIComponent(url));
const serverURL = urlObject.searchParams.get('realm');
let apiKey = urlObject.searchParams.get('otp_encrypted_login_key');
const desktopOtp = ConfigUtil.getConfigItem('desktopOtp');
apiKey = AuthUtil.hexToAscii(AuthUtil.xorStrings(apiKey, desktopOtp));
// Use this apiKey to login the realm if it exists
if (apiKey === '') {
console.log('Invalid API Key');
} else {
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) {
this.activateTab(index);
this.tabs[index].webview.loadURL(`${serverURL}/accounts/login/subdomain/${apiKey}`);
}
});
ConfigUtil.setConfigItem('desktopOtp', null);
}
});
ipcRenderer.on('show-network-error', (event: Event, index: number) => { ipcRenderer.on('show-network-error', (event: Event, index: number) => {
this.openNetworkTroubleshooting(index); this.openNetworkTroubleshooting(index);
}); });

View File

@@ -93,29 +93,33 @@ class GeneralSection extends BaseSection {
</div> </div>
<div class="title">${t.__('Advanced')}</div> <div class="title">${t.__('Advanced')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="enable-error-reporting"> <div class="setting-row" id="enable-error-reporting">
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div> <div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="show-download-folder"> <div class="setting-row" id="force-login-app">
<div class="setting-description">${t.__('Show downloaded files in file manager')}</div> <div class="setting-description">${t.__('Force social login in app instead of browser')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="add-custom-css"> <div class="setting-row" id="show-download-folder">
<div class="setting-description"> <div class="setting-description">${t.__('Show downloaded files in file manager')}</div>
${t.__('Add custom CSS')} <div class="setting-control"></div>
</div> </div>
<button class="custom-css-button green">${t.__('Upload')}</button> <div class="setting-row" id="add-custom-css">
</div> <div class="setting-description">
<div class="setting-row" id="remove-custom-css"> ${t.__('Add custom CSS')}
<div class="setting-description"> </div>
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div> <button class="custom-css-button green">${t.__('Upload')}</button>
</div> </div>
<div class="action red" id="css-delete-action"> <div class="setting-row" id="remove-custom-css">
<i class="material-icons">indeterminate_check_box</i> <div class="setting-description">
<span>${t.__('Delete')}</span> <div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div> </div>
</div> <div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>${t.__('Delete')}</span>
</div>
</div>
<div class="setting-row" id="download-folder"> <div class="setting-row" id="download-folder">
<div class="setting-description"> <div class="setting-description">
${t.__('Default download location')} ${t.__('Default download location')}
@@ -131,7 +135,6 @@ class GeneralSection extends BaseSection {
<div class="setting-description">${t.__('Ask where to save files before downloading')}</div> <div class="setting-description">${t.__('Ask where to save files before downloading')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">${t.__('Reset Application Data')}</div> <div class="title">${t.__('Reset Application Data')}</div>
<div class="settings-card"> <div class="settings-card">
@@ -166,6 +169,7 @@ class GeneralSection extends BaseSection {
this.updateQuitOnCloseOption(); this.updateQuitOnCloseOption();
this.updatePromptDownloadOption(); this.updatePromptDownloadOption();
this.enableErrorReporting(); this.enableErrorReporting();
this.enableLoginInApp();
// Platform specific settings // Platform specific settings
@@ -367,6 +371,18 @@ class GeneralSection extends BaseSection {
}); });
} }
enableLoginInApp(): void {
this.generateSettingOption({
$element: document.querySelector('#force-login-app .setting-control'),
value: ConfigUtil.getConfigItem('loginInApp', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('loginInApp');
ConfigUtil.setConfigItem('loginInApp', newValue);
this.enableLoginInApp();
}
});
}
clearAppDataDialog(): void { clearAppDataDialog(): void {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.'; const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName()); const getAppPath = path.join(app.getPath('appData'), app.getName());

View File

@@ -11,6 +11,8 @@ import SetupSpellChecker from './spellchecker';
import isDev = require('electron-is-dev'); import isDev = require('electron-is-dev');
import LinkUtil = require('./utils/link-util'); import LinkUtil = require('./utils/link-util');
import params = require('./utils/params-util'); import params = require('./utils/params-util');
import AuthUtil = require('./utils/auth-util');
import ConfigUtil = require('./utils/config-util');
import NetworkError = require('./pages/network'); import NetworkError = require('./pages/network');
@@ -79,7 +81,24 @@ process.once('loaded', (): void => {
// To prevent failing this script on linux we need to load it after the document loaded // To prevent failing this script on linux we need to load it after the document loaded
document.addEventListener('DOMContentLoaded', (): void => { document.addEventListener('DOMContentLoaded', (): void => {
if (params.isPageParams()) { if (params.isPageParams()) {
// Get the default language of the server const authMethods = page_params.external_authentication_methods; // eslint-disable-line no-undef
const loginInApp = ConfigUtil.getConfigItem('loginInApp');
console.log(loginInApp);
if (authMethods && !loginInApp) {
for (const authMethod of authMethods) {
const { button_id_suffix } = authMethod;
const $socialButton = document.querySelector(`button[id$="${button_id_suffix}"]`);
if ($socialButton) {
$socialButton.addEventListener('click', event => {
event.preventDefault();
const socialLink = $socialButton.closest('form').action;
AuthUtil.openInBrowser(socialLink);
});
}
}
}
// Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef const serverLanguage = page_params.default_language; // eslint-disable-line no-undef
if (serverLanguage) { if (serverLanguage) {
// Init spellchecker // Init spellchecker

View File

@@ -0,0 +1,36 @@
import { remote } from 'electron';
import cryptoRandomString = require('crypto-random-string');
import ConfigUtil = require('./config-util');
const { shell } = remote;
class AuthUtil {
openInBrowser = (link: string) => {
const otp = cryptoRandomString({length: 64});
ConfigUtil.setConfigItem('desktopOtp', otp);
shell.openExternal(`${link}?desktop_flow_otp=${otp}`);
};
xorStrings = (a: string, b: string): string => {
if (a.length === b.length) {
return a
.split('')
.map((char, i) => (parseInt(a[i], 16) ^ parseInt(b[i], 16)).toString(16))
.join('')
.toUpperCase();
} else {
return '';
}
};
hexToAscii = (hex: string) => {
let ascii = '';
for (let i = 0; i < hex.length; i += 2) {
ascii += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return ascii;
};
}
export = new AuthUtil();

View File

@@ -115,5 +115,9 @@
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "Enter Full Screen",
"History Shortcuts": "History Shortcuts", "History Shortcuts": "History Shortcuts",
"Quit when the window is closed": "Quit when the window is closed" "Quit when the window is closed": "Quit when the window is closed",
"File": "File",
"Network and Proxy Settings": "Network and Proxy Settings",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Force social login in app instead of browser (not recommended)": "Force social login in app instead of browser (not recommended)"
} }

View File

@@ -64,6 +64,15 @@
"entitlementsInherit": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist",
"gatekeeperAssess": false "gatekeeperAssess": false
}, },
"protocols": [
{
"name": "zulip",
"role": "Viewer",
"schemes": [
"zulip"
]
}
],
"linux": { "linux": {
"category": "Chat;GNOME;GTK;Network;InstantMessaging", "category": "Chat;GNOME;GTK;Network;InstantMessaging",
"icon": "build/icon.icns", "icon": "build/icon.icns",

6
typings.d.ts vendored
View File

@@ -13,10 +13,12 @@ declare module 'fs-extra';
declare module 'wurl'; declare module 'wurl';
declare module 'i18n'; declare module 'i18n';
declare module 'backoff'; declare module 'backoff';
declare module 'crypto-random-string';
interface PageParamsObject { interface PageParamsObject {
realm_uri: string; realm_uri: string;
default_language: string; default_language: string;
external_authentication_methods: any;
} }
declare var page_params: PageParamsObject; declare var page_params: PageParamsObject;