mirror of
				https://github.com/zulip/zulip-desktop.git
				synced 2025-11-04 14:03:27 +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);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,6 +97,10 @@ class GeneralSection extends BaseSection {
 | 
				
			|||||||
						<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="force-login-app">
 | 
				
			||||||
 | 
											<div class="setting-description">${t.__('Force social login in app instead of browser')}</div>
 | 
				
			||||||
 | 
											<div class="setting-control"></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
					<div class="setting-row" id="show-download-folder">
 | 
										<div class="setting-row" id="show-download-folder">
 | 
				
			||||||
						<div class="setting-description">${t.__('Show downloaded files in file manager')}</div>
 | 
											<div class="setting-description">${t.__('Show downloaded files in file manager')}</div>
 | 
				
			||||||
						<div class="setting-control"></div>
 | 
											<div class="setting-control"></div>
 | 
				
			||||||
@@ -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,6 +81,23 @@ 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()) {
 | 
				
			||||||
 | 
							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
 | 
							// 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) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								typings.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								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