Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@ You have direct access to the Parse database through function calls, so you can
<base href="${mountPath}"/>
<script>
PARSE_DASHBOARD_PATH = "${mountPath}";
PARSE_DASHBOARD_OTP_AUTO_SUBMIT = ${config.otpAutoSubmit === false ? 'false' : 'true'};
</script>
<title>Parse Dashboard</title>
</head>
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
103 changes: 103 additions & 0 deletions src/lib/tests/OtpAutoSubmit.test.js
Original file line number Diff line number Diff line change
@@ -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;');
});
});
6 changes: 5 additions & 1 deletion src/login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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') {
Expand Down
Loading