Token Validation in Electron

Created on: September 28, 2024

Answered using OpenAI 01 Mini by OpenAI01

Question

const { app, BrowserWindow, session } = require('electron');

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
devTools: false, // 禁用主窗口开发者工具
},
});

// 禁用 F12 和 Ctrl+Shift+I 开发者工具快捷键
win.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
event.preventDefault(); // 禁止 F12 和 Ctrl+Shift+I
}
});

// 处理新窗口的打开行为
win.webContents.setWindowOpenHandler(({ url }) => {
// 如果你想禁止所有新窗口弹出,可以返回 { action: 'deny' }
return {
action: 'allow', // 允许新窗口打开
overrideBrowserWindowOptions: {
webPreferences: {
devTools: false, // 禁用新窗口的开发者工具
nodeIntegration: false,
contextIsolation: true,
}
}
};
});

// 处理新窗口的创建
app.on('web-contents-created', (event, contents) => {
// 拦截所有窗口和 WebView 创建事件
if (contents.getType() === 'webview' || contents.getType() === 'window') {
// 禁用新窗口的开发者工具
contents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
event.preventDefault(); // 禁用新窗口的快捷键
}
});
contents.on('devtools-opened', () => {
contents.closeDevTools(); // 强制关闭任何已打开的开发者工具
});
}
});


win.loadURL( 'https://foo.bar' );
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
===========
在 win.loadURL( 'https://foo.bar' );之前加一段程序,先弹出一个输入框,输入一个Token字符串,这个字符串有2部分组成,中间用"mclubmclub"隔开,例如20240101mclubmclubhttp://foo.bar,前一部分是这个Token字符串的expireDate,后面一部分是Token后面win.loadURL的首页。输入完字符串后js要判断如果过期则提示用户并退出系统,如果没过期则load首页。所有程序都只放在一个main.js中完成,输入框也放在这个唯一的js文件中

Thought

显示输入提示

使用创建 BrowserWindow 的代码,接着显示一个输入对话框,要求用户输入 Token 字符串。然后解析该字符串,检查是否过期,根据结果加载相应的 URL。

创建对话框窗口

我在考虑创建一个对话框窗口,让用户输入Token。然而,由于Electron的对话框功能有限,使用渲染进程中的同步提示功能更为安全。可以考虑通过主进程的readline获取输入,不过这不是一个可视界面。或者,使用一个基于HTML的提示窗口由主进程创建,请求Token。

创建和校验

我正在创建一个 modal BrowserWindow,包含一个输入 token 的 HTML 表单。用户输入后,将验证 token,并决定继续或退出。

创建新窗口

我正在设想一个新的浏览器窗口来处理token输入。这个窗口会在应用启动时出现,询问token并验证其有效性,随后生成主窗口加载指定URL,或显示错误信息并退出。

探寻解决方案

我正在思考主进程如何通过预加载脚本处理来自提示窗口输入。这样做既保证了安全性,也方便了项目的管理。

设计输入窗口

我正在规划一个集成输入提示功能的代码,使用 Electron 的对话框或新窗口。接下来,我将编写代码以创建提示窗口,处理输入,进行验证,并最终创建主窗口。

建立窗口并验证

我创建了一个新的浏览器窗口 loading 从数据 URL 加载的 HTML。这个 HTML 表单提字符串令牌,并用脚本处理提交事件。确保 token 的有效期和格式。

创建输入框

Answer

To achieve the desired functionality in your Electron application—prompting the user for a Token string, validating it, and then loading the appropriate URL—you can modify your main.js as follows. This implementation includes an input dialog for the Token, validates the expiration date, and conditionally loads the specified URL or exits the application based on the validation result.

Here's the complete main.js with detailed explanations:

const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); /** * Creates a modal window to prompt the user for the Token input. * Returns a promise that resolves with the entered token. */ function createTokenWindow() { return new Promise((resolve, reject) => { // Create a hidden BrowserWindow for the Token input const tokenWin = new BrowserWindow({ width: 400, height: 200, resizable: false, modal: true, show: false, webPreferences: { nodeIntegration: true, // Enable Node integration for IPC contextIsolation: false, // Disable context isolation for simplicity }, }); // Load an HTML string with a simple form for Token input tokenWin.loadURL(`data:text/html, <html> <head> <title>Enter Token</title> <style> body { font-family: Arial, sans-serif; padding: 20px; } input { width: 100%; padding: 8px; margin-bottom: 10px; } button { padding: 8px 16px; } </style> </head> <body> <h2>Enter Token</h2> <form id="token-form"> <input type="text" id="token-input" placeholder="Enter Token" required /> <button type="submit">Submit</button> </form> <script> const { ipcRenderer } = require('electron'); const form = document.getElementById('token-form'); form.addEventListener('submit', (e) => { e.preventDefault(); const token = document.getElementById('token-input').value.trim(); ipcRenderer.send('token-submitted', token); }); </script> </body> </html> `); // Show the window once it's ready tokenWin.once('ready-to-show', () => { tokenWin.show(); }); // Listen for the 'token-submitted' event from the renderer process ipcMain.once('token-submitted', (event, token) => { resolve(token); tokenWin.close(); }); // Handle window closure without submitting a token tokenWin.on('closed', () => { reject(new Error('Token input was cancelled.')); }); }); } /** * Validates the Token string. * @param {string} token - The Token string entered by the user. * @returns {{ expireDate: Date, url: string }} - The parsed expire date and URL. * @throws Will throw an error if the token format is invalid or expired. */ function validateToken(token) { const separator = 'mclubmclub'; const parts = token.split(separator); if (parts.length !== 2) { throw new Error('Token format is invalid. Expected format: expireDatemclubmclubURL'); } const [expireDateStr, url] = parts; const expireDate = new Date(expireDateStr); const today = new Date(); if (isNaN(expireDate.getTime())) { throw new Error('Expire date in Token is invalid.'); } if (expireDate < today) { throw new Error('The Token has expired.'); } // Optional: Validate the URL format try { new URL(url); } catch (e) { throw new Error('URL in Token is invalid.'); } return { expireDate, url }; } /** * Creates the main application window with the specified URL. * @param {string} url - The URL to load in the main window. */ function createMainWindow(url) { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: false, contextIsolation: true, devTools: false, // Disable developer tools }, }); // Disable F12 and Ctrl+Shift+I developer tools shortcuts win.webContents.on('before-input-event', (event, input) => { if ( input.key === 'F12' || (input.control && input.shift && input.key.toUpperCase() === 'I') ) { event.preventDefault(); // Disable F12 and Ctrl+Shift+I } }); // Handle new window openings win.webContents.setWindowOpenHandler(({ url: newUrl }) => { return { action: 'allow', // Allow the new window to open overrideBrowserWindowOptions: { webPreferences: { devTools: false, // Disable dev tools in the new window nodeIntegration: false, contextIsolation: true, }, }, }; }); // Handle creation of web contents (e.g., new windows or webviews) app.on('web-contents-created', (event, contents) => { if (contents.getType() === 'window' || contents.getType() === 'webview') { // Disable developer tools shortcuts in new windows/webviews contents.on('before-input-event', (event, input) => { if ( input.key === 'F12' || (input.control && input.shift && input.key.toUpperCase() === 'I') ) { event.preventDefault(); // Disable F12 and Ctrl+Shift+I } }); // Force close any opened developer tools contents.on('devtools-opened', () => { contents.closeDevTools(); }); } }); // Load the specified URL win.loadURL(url); } /** * Initializes the application by prompting for the Token and creating the main window. */ async function initializeApp() { try { // Prompt the user for the Token const token = await createTokenWindow(); // Validate the Token const { url } = validateToken(token); // Create the main application window with the validated URL createMainWindow(url); } catch (error) { // Show an error dialog and quit the application dialog.showErrorBox('Error', error.message); app.quit(); } } // Initialize the app when Electron is ready app.whenReady().then(initializeApp); // Quit the app when all windows are closed, except on macOS app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); // Re-create a window on macOS when the dock icon is clicked and there are no other windows open app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { initializeApp(); } });

Explanation of the Implementation

  1. Token Input Window (createTokenWindow):

    • A modal BrowserWindow is created to prompt the user for the Token.
    • The window loads an inline HTML form using a data: URL. This form contains a text input for the Token and a submit button.
    • When the form is submitted, the Token is sent to the main process via IPC (ipcMain.once('token-submitted', ...)).
    • If the user closes the window without submitting, the promise is rejected, and the application exits gracefully.
  2. Token Validation (validateToken):

    • The entered Token is split using the separator "mclubmclub".
    • It checks if the Token has exactly two parts: expireDate and URL.
    • The expireDate is parsed and compared with the current date to determine if the Token has expired.
    • Optionally, the URL format is validated using the URL constructor.
    • If any validation fails, an error is thrown with an appropriate message.
  3. Main Window Creation (createMainWindow):

    • A new BrowserWindow is created with the specified width and height.
    • Developer tools are disabled both in the main window and in any new windows or webviews that may be opened.
    • Keyboard shortcuts for opening developer tools (F12 and Ctrl+Shift+I) are intercepted and disabled.
    • The validated URL from the Token is loaded in the main window.
  4. Application Initialization (initializeApp):

    • Orchestrates the flow by first prompting for the Token, validating it, and then creating the main window.
    • If any step fails (e.g., invalid Token, expired Token), an error dialog is shown, and the application exits.
  5. Application Lifecycle Handling:

    • Ensures that the application quits when all windows are closed, except on macOS where it's common for applications to stay active until the user quits explicitly.
    • Re-initializes the application if it's activated on macOS and no windows are open.

Security Considerations

  • Node Integration:

    • Node integration is enabled (nodeIntegration: true) only in the Token input window to facilitate IPC communication. It's disabled (nodeIntegration: false) in the main window to enhance security.
    • Caution: Enabling Node integration can expose your application to security risks, especially if you're loading remote content. Ensure that the Token input window does not load any untrusted content.
  • Context Isolation:

    • Context isolation is disabled (contextIsolation: false) in the Token input window to simplify IPC communication. In production applications, it's recommended to enable context isolation and use a preload script for secure IPC.
  • Input Validation:

    • The Token is thoroughly validated to prevent potential injection attacks or malformed data from causing unexpected behavior.

Running the Application

  1. Setup:

    • Ensure you have Node.js and Electron installed.
    • Initialize your project and install Electron if you haven't already:
      npm init -y npm install electron
  2. Save the main.js:

    • Replace your existing main.js with the provided code or adjust accordingly.
  3. Update package.json:

    • Ensure your package.json has the correct entry point and start script:
      { "name": "your-app-name", "version": "1.0.0", "main": "main.js", "scripts": { "start": "electron ." }, // ... other configurations }
  4. Run the Application:

    npm start
  5. Usage:

    • Upon starting, a dialog will prompt you to enter the Token.
    • Enter the Token in the format YYYYMMDDmclubmclubhttp://your-url.com, for example: 20240101mclubmclubhttps://foo.bar.
    • If the Token is valid and not expired, the main window will load the specified URL.
    • If the Token is invalid or expired, an error message will be displayed, and the application will exit.

Final Notes

  • Single main.js File:

    • All functionalities, including the Token prompt and main window creation, are encapsulated within a single main.js file as per your requirement.
  • Extensibility:

    • You can further enhance this implementation by adding more robust error handling, styling the Token input window, or integrating additional security measures as needed.

This setup ensures that users must provide a valid Token before accessing the main content of your Electron application, enhancing both functionality and security.

Share this Q&A