Skip to content

Commit

Permalink
feat: auto-submit one-time password (OTP) after entering (#2257)
Browse files Browse the repository at this point in the history
  • Loading branch information
dblythy committed Sep 14, 2022
1 parent 8ed6246 commit e528705
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 38 deletions.
21 changes: 12 additions & 9 deletions Parse-Dashboard/Authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
}
Expand All @@ -132,7 +135,7 @@ function authenticate(userToTest, usernameOnly) {
return {
isAuthenticated,
matchingUsername,
otpMissing,
otpMissingLength,
otpValid,
appsUserHasAccessTo,
isReadOnly,
Expand Down
58 changes: 32 additions & 26 deletions Parse-Dashboard/CLI/mfa.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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++;
Expand All @@ -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) {
Expand All @@ -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.` +
Expand Down Expand Up @@ -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([
{
Expand All @@ -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('');
Expand All @@ -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 });
}
};
4 changes: 2 additions & 2 deletions src/lib/tests/Authentication.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -55,7 +55,7 @@ function createAuthenticationResult(isAuthenticated, matchingUsername, appsUserH
matchingUsername,
appsUserHasAccessTo,
isReadOnly,
otpMissing: false,
otpMissingLength: false,
otpValid: true
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -95,7 +113,11 @@ export default class Login extends React.Component {
input={
<input
name='otpCode'
type='number'
type='text'
inputMode="numeric"
autoComplete='one-time-code'
pattern="[0-9]*"
onChange={e => updateField('otp', e)}
ref={this.inputRefMfa}
/>
} />
Expand Down

0 comments on commit e528705

Please sign in to comment.