From 7f3fbde8868f787c3130310a282a5b61c0b566f3 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 8 Sep 2022 17:43:47 +1000 Subject: [PATCH 1/7] feat: autosubmit otp depending on length --- Parse-Dashboard/Authentication.js | 21 ++++++++++++--------- Parse-Dashboard/CLI/mfa.js | 21 ++++++++++++++++----- src/login/Login.js | 23 ++++++++++++++++++++++- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index 6a859fa546..262c40a5ec 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..620042ca95 100644 --- a/Parse-Dashboard/CLI/mfa.js +++ b/Parse-Dashboard/CLI/mfa.js @@ -101,14 +101,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.` + @@ -189,6 +189,12 @@ module.exports = { if (algorithm !== 'SHA1') { data.mfaAlgorithm = algorithm; } + if (digits !== 6) { + data.mfaDigits = digits; + } + if (period !== 30) { + data.mfaPeriod = period; + } showQR(data.url); } @@ -214,12 +220,17 @@ module.exports = { const { url, secret } = generateSecret({ app, username, algorithm, digits, period }); showQR(url); - // Compose config const config = { mfa: secret }; if (algorithm !== 'SHA1') { config.mfaAlgorithm = algorithm; } + if (digits !== 6) { + config.mfaDigits = digits; + } + if (period !== 30) { + config.mfaPeriod = period; + } showInstructions({ app, username, secret, url, config }); } }; diff --git a/src/login/Login.js b/src/login/Login.js index 51f0331aaf..8ce0db85f3 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -16,20 +16,30 @@ 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.state = { forgot: false, username: sessionStorage.getItem('username') || '', - password: sessionStorage.getItem('password') || '' + password: sessionStorage.getItem('password') || '', + otp: '' }; sessionStorage.clear(); setBasePath(props.path); this.inputRefUser = React.createRef(); this.inputRefPass = React.createRef(); this.inputRefMfa = React.createRef(); + this.otpLength = otpLength; } componentDidMount() { @@ -53,6 +63,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); @@ -96,6 +115,8 @@ export default class Login extends React.Component { updateField('otp', e)} ref={this.inputRefMfa} /> } /> From e4e5700f7a328850a4116d13596ae3fd3e58ddc3 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 8 Sep 2022 17:52:02 +1000 Subject: [PATCH 2/7] fix tests --- src/lib/tests/Authentication.test.js | 4 ++-- src/login/Login.js | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) 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 8ce0db85f3..10998e70a0 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -31,8 +31,7 @@ export default class Login extends React.Component { this.state = { forgot: false, username: sessionStorage.getItem('username') || '', - password: sessionStorage.getItem('password') || '', - otp: '' + password: sessionStorage.getItem('password') || '' }; sessionStorage.clear(); setBasePath(props.path); @@ -115,7 +114,6 @@ export default class Login extends React.Component { updateField('otp', e)} ref={this.inputRefMfa} /> From 015d21341d9be57d602fc4dc7c89f8105c51dd77 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 8 Sep 2022 18:00:37 +1000 Subject: [PATCH 3/7] Update Authentication.js --- Parse-Dashboard/Authentication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index 262c40a5ec..0a6beee442 100644 --- a/Parse-Dashboard/Authentication.js +++ b/Parse-Dashboard/Authentication.js @@ -110,7 +110,7 @@ function authenticate(userToTest, usernameOnly) { algorithm: user.mfaAlgorithm || 'SHA1', secret: OTPAuth.Secret.fromBase32(user.mfa), digits: user.mfaDigits, - period: user.mfaPeriod, + period: user.mfaPeriod, }); const valid = totp.validate({ token: userToTest.otpCode From 1db581a7639a5d81bc47b62e0dac72de12584479 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 12 Sep 2022 10:12:00 +1000 Subject: [PATCH 4/7] Update mfa.js --- Parse-Dashboard/CLI/mfa.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Parse-Dashboard/CLI/mfa.js b/Parse-Dashboard/CLI/mfa.js index 620042ca95..6448757eb6 100644 --- a/Parse-Dashboard/CLI/mfa.js +++ b/Parse-Dashboard/CLI/mfa.js @@ -189,10 +189,10 @@ module.exports = { if (algorithm !== 'SHA1') { data.mfaAlgorithm = algorithm; } - if (digits !== 6) { + if (digits != 6) { data.mfaDigits = digits; } - if (period !== 30) { + if (period != 30) { data.mfaPeriod = period; } showQR(data.url); @@ -225,10 +225,10 @@ module.exports = { if (algorithm !== 'SHA1') { config.mfaAlgorithm = algorithm; } - if (digits !== 6) { + if (digits != 6) { config.mfaDigits = digits; } - if (period !== 30) { + if (period != 30) { config.mfaPeriod = period; } showInstructions({ app, username, secret, url, config }); From ee7019da280cdedcf81b0e8945a6c587d893cb7f Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 13 Sep 2022 10:04:54 +1000 Subject: [PATCH 5/7] Update Login.js --- src/login/Login.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/login/Login.js b/src/login/Login.js index 10998e70a0..514acf7f93 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -24,7 +24,7 @@ export default class Login extends React.Component { this.errors = json.text otpLength = json.otpLength; } catch (e) { - /* */ + console.log(`could not pass error json: ${e}`); } } @@ -113,7 +113,10 @@ export default class Login extends React.Component { input={ updateField('otp', e)} ref={this.inputRefMfa} /> From 5f2afa7bea4ca83320ce6131cfcfdf118a6cd45b Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 13 Sep 2022 10:16:50 +1000 Subject: [PATCH 6/7] Update mfa.js --- Parse-Dashboard/CLI/mfa.js | 61 +++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/Parse-Dashboard/CLI/mfa.js b/Parse-Dashboard/CLI/mfa.js index 6448757eb6..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) { @@ -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,24 +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; - } - if (digits != 6) { - data.mfaDigits = digits; - } - if (period != 30) { - data.mfaPeriod = period; - } - 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(''); @@ -218,19 +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; - } - if (digits != 6) { - config.mfaDigits = digits; - } - if (period != 30) { - config.mfaPeriod = period; - } - showInstructions({ app, username, secret, url, config }); + showInstructions({ app, username, config }); } }; From 63b3c427fdcc60cd87f888cb3e85e82d323f8bce Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 14 Sep 2022 14:55:05 +1000 Subject: [PATCH 7/7] Update Login.js --- src/login/Login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/login/Login.js b/src/login/Login.js index 514acf7f93..79f4de1b23 100644 --- a/src/login/Login.js +++ b/src/login/Login.js @@ -24,7 +24,7 @@ export default class Login extends React.Component { this.errors = json.text otpLength = json.otpLength; } catch (e) { - console.log(`could not pass error json: ${e}`); + this.errors = `could not pass error json: ${e}`; } }