mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-11-01 20:43:33 +00:00
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:
@@ -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');
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
36
app/renderer/js/utils/auth-util.ts
Normal file
36
app/renderer/js/utils/auth-util.ts
Normal 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();
|
||||||
@@ -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)"
|
||||||
}
|
}
|
||||||
@@ -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
6
typings.d.ts
vendored
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user