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 all 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 multi-factor authentication to dashboard login. To use one-time password, run `parse-dashboard --createMFA` or `parse-dashboard --createUser`. (Daniel Blyth) [#1624](https://github.com/parse-community/parse-dashboard/pull/1624)

## Improvements
- CI now pushes docker images to Docker Hub (Corey Baker) [#1781](https://github.com/parse-community/parse-dashboard/pull/1781)
- Add CI check to add changelog entry (Manuel Trezza) [#1764](https://github.com/parse-community/parse-dashboard/pull/1764)
Expand Down
34 changes: 31 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 OTPAuth = require('otpauth')

/**
* 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 one-time password.' });
}
if (!match.otpValid) {
return cb(null, false, { message: 'Invalid one-time password.' });
}
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 @@ -91,6 +102,22 @@ function authenticate(userToTest, usernameOnly) {
this.validUsers.find(user => {
let isAuthenticated = false;
let usernameMatches = userToTest.name == user.user;
if (usernameMatches && user.mfa && !usernameOnly) {
if (!userToTest.otpCode) {
otpMissing = true;
} else {
const totp = new OTPAuth.TOTP({
algorithm: user.mfaAlgorithm || 'SHA1',
secret: OTPAuth.Secret.fromBase32(user.mfa)
});
const valid = totp.validate({
token: userToTest.otpCode
});
if (valid === null) {
otpValid = false;
}
}
}
let passwordMatches = this.useEncryptedPasswords && !usernameOnly ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass;
if (usernameMatches && (usernameOnly || passwordMatches)) {
isAuthenticated = true;
Expand All @@ -99,13 +126,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
225 changes: 225 additions & 0 deletions Parse-Dashboard/CLI/mfa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
const crypto = require('crypto');
const inquirer = require('inquirer');
const OTPAuth = require('otpauth');
const { copy } = require('./utils.js');
const phrases = {
enterPassword: 'Enter a password:',
enterUsername: 'Enter a username:',
enterAppName: 'Enter the app name:',
}
const getAlgorithm = async () => {
let { algorithm } = await inquirer.prompt([
{
type: 'list',
name: 'algorithm',
message: 'Which hashing algorithm do you want to use?',
default: 'SHA1',
choices: [
'SHA1',
'SHA224',
'SHA256',
'SHA384',
'SHA512',
'SHA3-224',
'SHA3-256',
'SHA3-384',
'SHA3-512',
'Other'
]
}
]);
if (algorithm === 'Other') {
const result = await inquirer.prompt([
{
type: 'input',
name: 'algorithm',
message: 'Enter the hashing algorithm you want to use:'
}
]);
algorithm = result.algorithm;
}
const { digits, period } = await inquirer.prompt([
{
type: 'number',
name: 'digits',
default: 6,
message: 'Enter the number of digits the one-time password should have:'
},
{
type: 'number',
name: 'period',
default: 30,
message: 'Enter how long the one-time password should be valid (in seconds):'
}
])
return { algorithm, digits, period};
};
const generateSecret = ({ app, username, algorithm, digits, period }) => {
const secret = new OTPAuth.Secret();
const totp = new OTPAuth.TOTP({
issuer: app,
label: username,
algorithm,
digits,
period,
secret
});
const url = totp.toString();
return { secret: secret.base32, url };
};
const showQR = text => {
const QRCode = require('qrcode');
QRCode.toString(text, { type: 'terminal' }, (err, url) => {
console.log(
'\n------------------------------------------------------------------------------' +
`\n\n${url}`
);
});
};

const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, config }) => {
let orderCounter = 0;
const getOrder = () => {
orderCounter++;
return orderCounter;
}
console.log(
'------------------------------------------------------------------------------' +
'\n\nFollow these steps to complete the set-up:'
);

console.log(
`\n${getOrder()}. Add the following settings for user "${username}" ${app ? `in app "${app}" ` : '' }to the Parse Dashboard configuration.` +
`\n\n ${JSON.stringify(config)}`
);

if (passwordCopied) {
console.log(
`\n${getOrder()}. Securely store the generated login password that has been copied to your clipboard.`
);
}

if (secret) {
console.log(
`\n${getOrder()}. Open the authenticator app to scan the QR code above or enter this secret code:` +
`\n\n ${secret}` +
'\n\n If the secret code generates incorrect one-time passwords, try this alternative:' +
`\n\n ${url}` +
`\n\n${getOrder()}. Destroy any records of the QR code and the secret code to secure the account.`
);
}

if (encrypt) {
console.log(
`\n${getOrder()}. Make sure that "useEncryptedPasswords" is set to "true" in your dashboard configuration.` +
'\n You chose to generate an encrypted password for this user.' +
'\n Any existing users with non-encrypted passwords will require newly created, encrypted passwords.'
);
}
console.log(
'\n------------------------------------------------------------------------------\n'
);
}

module.exports = {
async createUser() {
const data = {};

console.log('');
const { username, password } = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: phrases.enterUsername
},
{
type: 'confirm',
name: 'password',
message: 'Do you want to auto-generate a password?'
}
]);
data.user = username;
if (!password) {
const { password } = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: phrases.enterPassword
}
]);
data.pass = password;
} else {
const password = crypto.randomBytes(20).toString('base64');
data.pass = password;
}
const { mfa, encrypt } = await inquirer.prompt([
{
type: 'confirm',
name: 'encrypt',
message: 'Should the password be encrypted? (strongly recommended, otherwise it is stored in clear-text)'
},
{
type: 'confirm',
name: 'mfa',
message: 'Do you want to enable multi-factor authentication?'
}
]);
if (encrypt) {
// Copy the raw password to clipboard
copy(data.pass);

// Encrypt password
const bcrypt = require('bcryptjs');
const salt = bcrypt.genSaltSync(10);
data.pass = bcrypt.hashSync(data.pass, salt);
}
if (mfa) {
const { app } = await inquirer.prompt([
{
type: 'input',
name: 'app',
message: phrases.enterAppName
}
]);
const { algorithm, digits, period } = await getAlgorithm();
const { secret, url } = generateSecret({ app, username, algorithm, digits, period });
data.mfa = secret;
data.app = app;
data.url = url;
if (algorithm !== 'SHA1') {
data.mfaAlgorithm = algorithm;
}
showQR(data.url);
}

const config = { mfa: data.mfa, user: data.user, pass: data.pass };
showInstructions({ app: data.app, username, passwordCopied: true, secret: data.mfa, url: data.url, encrypt, config });
},
async createMFA() {
console.log('');
const { username, app } = await inquirer.prompt([
{
type: 'input',
name: 'username',
message:
'Enter the username for which you want to enable multi-factor authentication:'
},
{
type: 'input',
name: 'app',
message: phrases.enterAppName
}
]);
const { algorithm, digits, period } = await getAlgorithm();

const { url, secret } = generateSecret({ app, username, algorithm, digits, period });
showQR(url);

// Compose config
const config = { mfa: secret };
if (algorithm !== 'SHA1') {
config.mfaAlgorithm = algorithm;
}
showInstructions({ app, username, secret, url, config });
}
};
7 changes: 7 additions & 0 deletions Parse-Dashboard/CLI/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
copy(text) {
const proc = require('child_process').spawn('pbcopy');
proc.stdin.write(text);
proc.stdin.end();
}
}
6 changes: 6 additions & 0 deletions Parse-Dashboard/CLIHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { createUser, createMFA } = require('./CLI/mfa');

module.exports = {
createUser,
createMFA
};
11 changes: 11 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 CLIHelper = require('./CLIHelper.js');

const program = require('commander');
program.option('--appId [appId]', 'the app Id of the app you would like to manage.');
Expand All @@ -28,9 +29,19 @@ 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 on trusted devices only.');
program.option('--createMFA', 'helper tool to allow you to generate multi-factor authentication secrets.');

program.parse(process.argv);

for (const key in program) {
const func = CLIHelper[key];
if (func && typeof func === 'function') {
func();
return;
}
}

const host = program.host || process.env.HOST || '0.0.0.0';
const port = program.port || process.env.PORT || 4040;
const mountPath = program.mountPath || process.env.MOUNT_PATH || '/';
Expand Down
Loading