diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index 6a859fa546..0a6beee442 100644 --- a/Parse-Dashboard/Authentication.js +++ b/Parse-Dashboard/Authentication.js @@ -30,13 +30,13 @@ function initialize(app, options) { 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.' }); + return cb(null, false, { message: JSON.stringify({ text: 'Invalid username or password' }) }); } if (!match.otpValid) { - return cb(null, false, { message: 'Invalid one-time password.' }); + return cb(null, false, { message: JSON.stringify({ text: 'Invalid one-time password.', otpLength: match.otpMissingLength || 6}) }); + } + if (match.otpMissingLength) { + return cb(null, false, { message: JSON.stringify({ text: 'Please enter your one-time password.', otpLength: match.otpMissingLength || 6 })}); } cb(null, match.matchingUsername); }) @@ -91,7 +91,7 @@ function authenticate(userToTest, usernameOnly) { let appsUserHasAccessTo = null; let matchingUsername = null; let isReadOnly = false; - let otpMissing = false; + let otpMissingLength = false; let otpValid = true; //they provided auth @@ -104,17 +104,20 @@ function authenticate(userToTest, usernameOnly) { let usernameMatches = userToTest.name == user.user; if (usernameMatches && user.mfa && !usernameOnly) { if (!userToTest.otpCode) { - otpMissing = true; + otpMissingLength = user.mfaDigits || 6; } else { const totp = new OTPAuth.TOTP({ algorithm: user.mfaAlgorithm || 'SHA1', - secret: OTPAuth.Secret.fromBase32(user.mfa) + secret: OTPAuth.Secret.fromBase32(user.mfa), + digits: user.mfaDigits, + period: user.mfaPeriod, }); const valid = totp.validate({ token: userToTest.otpCode }); if (valid === null) { otpValid = false; + otpMissingLength = user.mfaDigits || 6; } } } @@ -132,7 +135,7 @@ function authenticate(userToTest, usernameOnly) { return { isAuthenticated, matchingUsername, - otpMissing, + otpMissingLength, otpValid, appsUserHasAccessTo, isReadOnly, diff --git a/Parse-Dashboard/CLI/mfa.js b/Parse-Dashboard/CLI/mfa.js index f862f2cf45..3f26a73a98 100644 --- a/Parse-Dashboard/CLI/mfa.js +++ b/Parse-Dashboard/CLI/mfa.js @@ -65,7 +65,19 @@ const generateSecret = ({ app, username, algorithm, digits, period }) => { secret }); const url = totp.toString(); - return { secret: secret.base32, url }; + const config = { mfa: secret.base32 }; + config.app = app; + config.url = url; + if (algorithm !== 'SHA1') { + config.mfaAlgorithm = algorithm; + } + if (digits != 6) { + config.mfaDigits = digits; + } + if (period != 30) { + config.mfaPeriod = period; + } + return { config }; }; const showQR = text => { const QRCode = require('qrcode'); @@ -77,7 +89,10 @@ const showQR = text => { }); }; -const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, config }) => { +const showInstructions = ({ app, username, passwordCopied, encrypt, config }) => { + const {secret, url} = config; + const mfaJSON = {...config}; + delete mfaJSON.url; let orderCounter = 0; const getOrder = () => { orderCounter++; @@ -90,7 +105,7 @@ const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, 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)}` + `\n\n ${JSON.stringify(mfaJSON)}` ); if (passwordCopied) { @@ -101,14 +116,14 @@ const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, 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${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 ${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.` + @@ -173,6 +188,7 @@ module.exports = { const salt = bcrypt.genSaltSync(10); data.pass = bcrypt.hashSync(data.pass, salt); } + const config = {}; if (mfa) { const { app } = await inquirer.prompt([ { @@ -182,18 +198,13 @@ module.exports = { } ]); 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 secret =generateSecret({ app, username, algorithm, digits, period }); + Object.assign(config, secret.config); + showQR(secret.config.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 }); + config.user = data.user; + config.pass = data.pass ; + showInstructions({ app: data.app, username, passwordCopied: true, encrypt, config }); }, async createMFA() { console.log(''); @@ -212,14 +223,9 @@ module.exports = { ]); const { algorithm, digits, period } = await getAlgorithm(); - const { url, secret } = generateSecret({ app, username, algorithm, digits, period }); - showQR(url); - + const { config } = generateSecret({ app, username, algorithm, digits, period }); + showQR(config.url); // Compose config - const config = { mfa: secret }; - if (algorithm !== 'SHA1') { - config.mfaAlgorithm = algorithm; - } - showInstructions({ app, username, secret, url, config }); + showInstructions({ app, username, config }); } }; diff --git a/src/lib/tests/Authentication.test.js b/src/lib/tests/Authentication.test.js index 71a7249cd4..df2fe102de 100644 --- a/src/lib/tests/Authentication.test.js +++ b/src/lib/tests/Authentication.test.js @@ -10,7 +10,7 @@ jest.dontMock('bcryptjs'); const Authentication = require('../../../Parse-Dashboard/Authentication'); const apps = [{appId: 'test123'}, {appId: 'test789'}]; -const readOnlyApps = apps.map((app) => { +const readOnlyApps = apps.map((app) => { app.readOnly = true; return app; }); @@ -55,7 +55,7 @@ function createAuthenticationResult(isAuthenticated, matchingUsername, appsUserH matchingUsername, appsUserHasAccessTo, isReadOnly, - otpMissing: false, + otpMissingLength: false, otpValid: true } } diff --git a/src/login/Login.js b/src/login/Login.js index 51f0331aaf..79f4de1b23 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -16,8 +16,16 @@ export default class Login extends React.Component { super(); let errorDiv = document.getElementById('login_errors'); + let otpLength = 6; if (errorDiv) { this.errors = errorDiv.innerHTML; + try { + const json = JSON.parse(this.errors) + this.errors = json.text + otpLength = json.otpLength; + } catch (e) { + this.errors = `could not pass error json: ${e}`; + } } this.state = { @@ -30,6 +38,7 @@ export default class Login extends React.Component { this.inputRefUser = React.createRef(); this.inputRefPass = React.createRef(); this.inputRefMfa = React.createRef(); + this.otpLength = otpLength; } componentDidMount() { @@ -53,6 +62,15 @@ 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) { + const input = document.querySelectorAll('input'); + for (const field of input) { + if (field.type === 'submit') { + field.click(); + break; + } + } + } } const formSubmit = () => { sessionStorage.setItem('username', this.state.username); @@ -95,7 +113,11 @@ export default class Login extends React.Component { input={ updateField('otp', e)} ref={this.inputRefMfa} /> } />