Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MFA to Dashboard #1624

Merged
merged 40 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e9d3ea7
Update CloudCode.react.js
dblythy Nov 1, 2020
2a5c050
Allow Writing Cloud Code
dblythy Nov 2, 2020
0cb7894
Add MFA to Dashboard
dblythy Nov 17, 2020
c0d385f
Merge branch 'master' into MFA
dblythy Sep 4, 2021
3f382a8
Merge remote-tracking branch 'upstream/master' into MFA
dblythy Sep 4, 2021
a7af2f4
add inquirer
dblythy Sep 4, 2021
7cf9676
add changelog
dblythy Sep 4, 2021
9a4ffbd
Update index.js
dblythy Sep 4, 2021
c4a5c59
Update package.json
dblythy Sep 4, 2021
f6e5a98
Revert "Update CloudCode.react.js"
dblythy Sep 4, 2021
30540b3
Revert "Allow Writing Cloud Code"
dblythy Sep 4, 2021
f4a7412
Update index.js
dblythy Sep 4, 2021
6b1718a
Update README.md
dblythy Sep 4, 2021
7911009
Update index.js
dblythy Sep 4, 2021
40960f5
hide otp field by default
dblythy Sep 4, 2021
b9b3103
change to one-time
dblythy Sep 4, 2021
44e72c8
change to otp
dblythy Sep 4, 2021
d354063
fix package-lock
mtrezza Sep 4, 2021
90b167d
add readme
dblythy Sep 5, 2021
4a3a5ed
Update Authentication.js
dblythy Sep 5, 2021
9be7467
change to SHA256
dblythy Sep 5, 2021
d1b0295
Update CHANGELOG.md
dblythy Sep 5, 2021
7b7b921
Update README.md
dblythy Sep 5, 2021
15c5a11
use OTPAuth secrets
dblythy Sep 5, 2021
56b2084
Update index.js
dblythy Sep 5, 2021
2564cbf
Update index.js
dblythy Sep 5, 2021
eebf734
add cli helper
dblythy Sep 5, 2021
07a2448
change to SHA1
dblythy Sep 6, 2021
1a17255
add digits option
dblythy Sep 6, 2021
bf2d615
refactoring mfa flow
mtrezza Sep 7, 2021
d4b17c2
Merge branch 'master' into MFA
mtrezza Sep 7, 2021
981d096
more simplification
mtrezza Sep 7, 2021
c2cd85f
fixed unsafe instructions
mtrezza Sep 7, 2021
3e3bcad
fixed password copy to clipboard
mtrezza Sep 7, 2021
6d206e6
add newline before CLI questions
mtrezza Sep 7, 2021
63b4a6a
style
mtrezza Sep 7, 2021
e30c22a
refactored readme
mtrezza Sep 7, 2021
0654d8d
removed RASS
mtrezza Sep 7, 2021
0323752
replaced URL with secret
mtrezza Sep 7, 2021
312a842
added url and secret to output
mtrezza Sep 7, 2021
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
[Full Changelog](https://github.com/parse-community/parse-dashboard/compare/2.2.0...master)

## New Features
- Add MFA to dashboard login. To use MFA, run `parse-dashboard --createMFA` or `parse-dashboard --createUser`. (Daniel Blyth) [#1624](https://github.com/parse-community/parse-dashboard/pull/1624)

## Improvements
- Add CI check to add changelog entry (Manuel Trezza) [#1764](https://github.com/parse-community/parse-dashboard/pull/1764)
- Refactor: uniform issue templates across repos (Manuel Trezza) [#1767](https://github.com/parse-community/parse-dashboard/pull/1767)
Expand Down
26 changes: 23 additions & 3 deletions Parse-Dashboard/Authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var bcrypt = require('bcryptjs');
var csrf = require('csurf');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
const authenticator = require('otplib').authenticator;

/**
* Constructor for Authentication class
Expand All @@ -21,14 +22,22 @@ function initialize(app, options) {
options = options || {};
var self = this;
passport.use('local', new LocalStrategy(
function(username, password, cb) {
{passReqToCallback:true},
function(req, username, password, cb) {
var match = self.authenticate({
name: username,
pass: password
pass: password,
otpCode: req.body.otpCode
});
if (!match.matchingUsername) {
return cb(null, false, { message: 'Invalid username or password' });
}
if (match.otpMissing) {
return cb(null, false, { message: 'Please enter your OTP code.' });
}
if (!match.otpValid) {
return cb(null, false, { message: 'Invalid OTP code.' });
}
cb(null, match.matchingUsername);
})
);
Expand Down Expand Up @@ -82,6 +91,8 @@ function authenticate(userToTest, usernameOnly) {
let appsUserHasAccessTo = null;
let matchingUsername = null;
let isReadOnly = false;
let otpMissing = false;
let otpValid = true;

//they provided auth
let isAuthenticated = userToTest &&
Expand All @@ -90,6 +101,14 @@ function authenticate(userToTest, usernameOnly) {
//the provided auth matches one of the users
this.validUsers.find(user => {
let isAuthenticated = false;
if (user.mfa && !usernameOnly) {
if (!userToTest.otpCode) {
otpMissing = true;
}
if (!authenticator.verify({ token:userToTest.otpCode, secret: user.mfa })) {
otpValid = false;
dblythy marked this conversation as resolved.
Show resolved Hide resolved
}
}
let usernameMatches = userToTest.name == user.user;
let passwordMatches = this.useEncryptedPasswords && !usernameOnly ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass;
if (usernameMatches && (usernameOnly || passwordMatches)) {
Expand All @@ -99,13 +118,14 @@ function authenticate(userToTest, usernameOnly) {
appsUserHasAccessTo = user.apps || null;
isReadOnly = !!user.readOnly; // make it true/false
}

return isAuthenticated;
}) ? true : false;

return {
isAuthenticated,
matchingUsername,
otpMissing,
otpValid,
appsUserHasAccessTo,
isReadOnly,
};
Expand Down
79 changes: 79 additions & 0 deletions Parse-Dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const path = require('path');
const jsonFile = require('json-file-plus');
const express = require('express');
const parseDashboard = require('./app');
const authenticator = require('otplib').authenticator;

const program = require('commander');
program.option('--appId [appId]', 'the app Id of the app you would like to manage.');
Expand All @@ -28,6 +29,8 @@ program.option('--sslKey [sslKey]', 'the path to the SSL private key.');
program.option('--sslCert [sslCert]', 'the path to the SSL certificate.');
program.option('--trustProxy [trustProxy]', 'set this flag when you are behind a front-facing proxy, such as when hosting on Heroku. Uses X-Forwarded-* headers to determine the client\'s connection and IP address.');
program.option('--cookieSessionSecret [cookieSessionSecret]', 'set the cookie session secret, defaults to a random string. You should set that value if you want sessions to work across multiple server, or across restarts');
program.option('--createUser', 'helper tool to allow you to generate secure user passwords and secrets. Use this once on a trusted device only.');
program.option('--createMFA', 'helper tool to allow you to generate MFA secrets.');

program.parse(process.argv);

Expand Down Expand Up @@ -57,6 +60,82 @@ let configUserPassword = program.userPassword || process.env.PARSE_DASHBOARD_USE
let configSSLKey = program.sslKey || process.env.PARSE_DASHBOARD_SSL_KEY;
let configSSLCert = program.sslCert || process.env.PARSE_DASHBOARD_SSL_CERT;

const showQR = (text) => {
const QRCode = require('qrcode')
QRCode.toString(text, {type:'terminal'}, (err, url) => {
console.log(url)
})
}
if (program.createUser) {
(async () => {
const inquirer = require('inquirer');
const result = {}
const displayResult = {};
const {username, password} = await inquirer.prompt([{
type: 'input',
name: 'username',
message: 'Please enter the username:',
}, {
type: 'confirm',
name: 'password',
message: 'Would you like to generate a secure password?',
}]);
displayResult.username = username;
result.user = username;
if (!password) {
const {password} = await inquirer.prompt([{
type: 'password',
name: 'password',
message: `Please enter the password for ${username}:`,
}]);
displayResult.password = password;
result.pass = password
} else {
const password = require('crypto').randomBytes(20).toString('hex');
const bcrypt = require('bcryptjs');
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(password, salt);
result.pass = hash;
displayResult.password = password;
}
const {mfa} = await inquirer.prompt([{
type: 'confirm',
name: 'mfa',
message: `Would you like to an MFA secret for ${username}?`,
}])
if (mfa) {
const secret = authenticator.generateSecret();
result.mfa = secret;
displayResult.mfa = authenticator.keyuri(username, configAppName || 'Parse Dashboard', secret);
}
const proc = require('child_process').spawn('pbcopy');
proc.stdin.write(JSON.stringify(displayResult));
proc.stdin.end();
console.log(`\n\nYour new user details' raw credentials have been copied to your clipboard. Add the following to your Parse Dashboard config:\n\n${JSON.stringify(result)}\n\n`);
if (displayResult.mfa) {
showQR(displayResult.mfa)
console.log(`After you've shared the QR code ${username}, it is recommended to delete any photos or records of it.\n`)
}
})();
return;
}
if (program.createMFA) {
(async () => {
const inquirer = require('inquirer');
const {username} = await inquirer.prompt([{
type: 'input',
name: 'username',
message: 'Please enter the name of the user you would like to create MFA for:',
}]);
const secret = authenticator.generateSecret();
console.log(`Please add this to your dashboard config for ${username}.\n\n"mfa":"${secret}"\n\n\n\n\nAsk ${username} to install an Authenticator app and scan this QR code on their device:\n`)
const url = authenticator.keyuri(username, configAppName || 'Parse Dashboard', secret);
showQR(url);
console.log(`After you've shared the QR code ${username}, it is recommended to delete any photos or records of it.\n`)
})();
return;
}

function handleSIGs(server) {
const signals = {
'SIGINT': 2,
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ parse-dashboard --dev --appId yourAppId --masterKey yourMasterKey --serverURL "h

You may set the host, port and mount path by supplying the `--host`, `--port` and `--mountPath` options to parse-dashboard. You can use anything you want as the app name, or leave it out in which case the app ID will be used.

NB: the `--dev` parameter is disabling production-ready security features, do not use this parameter when starting the dashboard in production. This parameter is useful if you are running on docker.
NB: the `--dev` parameter is disabling production-ready security features, do not use this parameter when starting the dashboard in production. This parameter is useful if you are running on docker.

After starting the dashboard, you can visit http://localhost:4040 in your browser:

Expand Down Expand Up @@ -245,7 +245,7 @@ You can set `appNameForURL` in the config file for each app to control the url o

To change the app to production, simply set `production` to `true` in your config file. The default value is false if not specified.

### Prevent columns sorting
### Prevent columns sorting

You can prevent some columns to be sortable by adding `preventSort` to columnPreference options in each app configuration

Expand Down Expand Up @@ -378,7 +378,7 @@ You can configure your dashboard for Basic Authentication by adding usernames an
```

You can store the password in either `plain text` or `bcrypt` formats. To use the `bcrypt` format, you must set the config `useEncryptedPasswords` parameter to `true`.
You can encrypt the password using any online bcrypt tool e.g. [https://www.bcrypt-generator.com](https://www.bcrypt-generator.com).
You can generate encrypted passwords by using `parse-dashboard --createUser`, and pasting the result in your users config.

### Separating App Access Based on User Identity
If you have configured your dashboard to manage multiple applications, you can restrict the management of apps based on user identity.
Expand Down Expand Up @@ -452,15 +452,15 @@ You can mark a user as a read-only user:
"appId": "myAppId1",
"masterKey": "myMasterKey1",
"readOnlyMasterKey": "myReadOnlyMasterKey1",
"serverURL": "myURL1",
"serverURL": "myURL1",
"port": 4040,
"production": true
},
{
"appId": "myAppId2",
"masterKey": "myMasterKey2",
"readOnlyMasterKey": "myReadOnlyMasterKey2",
"serverURL": "myURL2",
"serverURL": "myURL2",
"port": 4041,
"production": true
}
Expand Down Expand Up @@ -495,7 +495,7 @@ You can give read only access to a user on a per-app basis:
"appId": "myAppId1",
"masterKey": "myMasterKey1",
"readOnlyMasterKey": "myReadOnlyMasterKey1",
"serverURL": "myURL",
"serverURL": "myURL",
"port": 4040,
"production": true
},
Expand Down Expand Up @@ -536,7 +536,7 @@ You can provide a list of locales or languages you want to support for your dash

## Run with Docker

The official docker image is published on [docker hub](https://hub.docker.com/r/parseplatform/parse-dashboard)
The official docker image is published on [docker hub](https://hub.docker.com/r/parseplatform/parse-dashboard)

Run the image with your ``config.json`` mounted as a volume

Expand Down
Loading