From 910ac511210e8f414ed4bddfca455e95f0c4ebf9 Mon Sep 17 00:00:00 2001 From: vaidehisheth Date: Thu, 3 Sep 2026 13:11:52 +0530 Subject: [PATCH] feat: Add option `otpAutoSubmit` to control auto-submitting the login form after entering a one-time password With MFA enabled, the login form is submitted automatically as soon as the entered one-time password reaches the expected length. This behavior is hardcoded and cannot be disabled, which means a user has no chance to review or correct the code before it is submitted; a single mistyped digit causes a failed login and a full page reload. Adds a new root-level option `otpAutoSubmit` (default `true`) that gates the behavior. Existing installations are unaffected; setting it to `false` requires the user to submit the login form manually. The option is passed to the login page by injecting a `PARSE_DASHBOARD_OTP_AUTO_SUBMIT` global into the page shell, following the existing `enableResourceCache` pattern. Co-Authored-By: Claude Opus 5 --- Parse-Dashboard/app.js | 1 + README.md | 11 +++ src/lib/tests/OtpAutoSubmit.test.js | 103 ++++++++++++++++++++++++++++ src/login/Login.js | 6 +- 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/lib/tests/OtpAutoSubmit.test.js diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 31ea47637c..6cf7629b43 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -1213,6 +1213,7 @@ You have direct access to the Parse database through function calls, so you can Parse Dashboard diff --git a/README.md b/README.md index 7748387290..6f70706233 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ This section provides a comprehensive reference for all Parse Dashboard configur | `iconsFolder` | String | No | - | - | - | `"icons"` | Folder for app icons (relative or absolute path) | - | | `agent` | Object | No | - | - | `PARSE_DASHBOARD_AGENT` (JSON) | `{...}` | AI agent configuration | [AI Agent Configuration](#ai-agent) | | `enableResourceCache` | Boolean | No | `false` | - | - | `true` | Enable browser caching of dashboard resources | - | +| `otpAutoSubmit` | Boolean | No | `true` | - | - | `false` | Automatically submit the login form once a complete one-time password has been entered | [Multi-Factor Authentication](#multi-factor-authentication-one-time-password) | ##### App Options @@ -779,6 +780,16 @@ If you create a new user by running `parse-dashboard --createUser`, you will be Parse Dashboard follows the industry standard and supports the common OTP algorithm `SHA-1` by default, to be compatible with most authenticator apps. If you have specific security requirements regarding TOTP characteristics (algorithm, digit length, time period) you can customize them by using the guided configuration mentioned above. +By default, the login form is submitted automatically as soon as a one-time password of the expected length has been entered, so the user does not have to click the login button. If you prefer the user to review the entered one-time password and submit the form manually, set the `otpAutoSubmit` option to `false`: + +```json +{ + "apps": [{"...": "..."}], + "otpAutoSubmit": false, + "users": [{"...": "..."}] +} +``` + ### Running Multiple Dashboard Replicas When deploying Parse Dashboard with multiple replicas behind a load balancer, you need to use a shared session store to ensure that CSRF tokens and user sessions work correctly across all replicas. Without a shared session store, login attempts may fail with "CSRF token validation failed" errors when requests are distributed across different replicas. diff --git a/src/lib/tests/OtpAutoSubmit.test.js b/src/lib/tests/OtpAutoSubmit.test.js new file mode 100644 index 0000000000..cc5ea66a91 --- /dev/null +++ b/src/lib/tests/OtpAutoSubmit.test.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +jest.dontMock('../../../Parse-Dashboard/Authentication.js'); +jest.dontMock('../../../Parse-Dashboard/app.js'); + +const express = require('express'); +const http = require('http'); + +const SESSION_SECRET = 'test-secret'; + +/** + * Fetch the login page, which is the only page that carries the OTP + * auto-submit flag. + */ +function getLoginPage(port) { + return new Promise((resolve, reject) => { + const req = http.request( + { hostname: '127.0.0.1', port, path: '/login', method: 'GET' }, + res => { + let data = ''; + res.on('data', chunk => (data += chunk)); + res.on('end', () => resolve({ status: res.statusCode, raw: data })); + } + ); + req.on('error', reject); + req.end(); + }); +} + +/** + * Build a dashboard config with users, since the login page is only served + * when users are configured. + */ +function configWithUsers(overrides = {}) { + return { + apps: [ + { + serverURL: 'http://localhost:1337/parse', + appId: 'testAppId', + masterKey: 'testMasterKey', + appName: 'TestApp', + }, + ], + users: [{ user: 'admin', pass: 'admin' }], + ...overrides, + }; +} + +function startDashboard(config) { + return new Promise(resolve => { + const parseDashboard = require('../../../Parse-Dashboard/app.js'); + const parentApp = express(); + parentApp.use('/', parseDashboard(config, { cookieSessionSecret: SESSION_SECRET })); + + const server = parentApp.listen(0, () => resolve({ server, port: server.address().port })); + }); +} + +function stopDashboard(server) { + return new Promise(resolve => (server ? server.close(resolve) : resolve())); +} + +describe('OTP auto-submit option', () => { + let server; + + afterEach(async () => { + await stopDashboard(server); + server = undefined; + }); + + it('enables auto-submit when the option is not set', async () => { + let port; + ({ server, port } = await startDashboard(configWithUsers())); + + const res = await getLoginPage(port); + + expect(res.status).toBe(200); + expect(res.raw).toContain('PARSE_DASHBOARD_OTP_AUTO_SUBMIT = true;'); + }); + + it('enables auto-submit when the option is set to true', async () => { + let port; + ({ server, port } = await startDashboard(configWithUsers({ otpAutoSubmit: true }))); + + const res = await getLoginPage(port); + + expect(res.raw).toContain('PARSE_DASHBOARD_OTP_AUTO_SUBMIT = true;'); + }); + + it('disables auto-submit when the option is set to false', async () => { + let port; + ({ server, port } = await startDashboard(configWithUsers({ otpAutoSubmit: false }))); + + const res = await getLoginPage(port); + + expect(res.raw).toContain('PARSE_DASHBOARD_OTP_AUTO_SUBMIT = false;'); + }); +}); diff --git a/src/login/Login.js b/src/login/Login.js index 15d00b872e..70bfd58ece 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -42,6 +42,10 @@ export default class Login extends React.Component { this.inputRefPass = React.createRef(); this.inputRefMfa = React.createRef(); this.otpLength = otpLength; + // Auto-submitting the login form once the full one-time password has been + // entered is enabled by default and can be turned off via the + // `otpAutoSubmit` dashboard option. + this.otpAutoSubmit = window.PARSE_DASHBOARD_OTP_AUTO_SUBMIT !== false; } componentDidMount() { @@ -65,7 +69,7 @@ export default class Login extends React.Component { const { path } = this.props; const updateField = (field, e) => { this.setState({ [field]: e.target.value }); - if (field === 'otp' && e.target.value.length >= this.otpLength) { + if (this.otpAutoSubmit && field === 'otp' && e.target.value.length >= this.otpLength) { const input = document.querySelectorAll('input'); for (const field of input) { if (field.type === 'submit') {