Skip to content

Commit e528705

Browse files
authored
feat: auto-submit one-time password (OTP) after entering (#2257)
1 parent 8ed6246 commit e528705

File tree

4 files changed

+69
-38
lines changed

4 files changed

+69
-38
lines changed

Parse-Dashboard/Authentication.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ function initialize(app, options) {
3030
otpCode: req.body.otpCode
3131
});
3232
if (!match.matchingUsername) {
33-
return cb(null, false, { message: 'Invalid username or password' });
34-
}
35-
if (match.otpMissing) {
36-
return cb(null, false, { message: 'Please enter your one-time password.' });
33+
return cb(null, false, { message: JSON.stringify({ text: 'Invalid username or password' }) });
3734
}
3835
if (!match.otpValid) {
39-
return cb(null, false, { message: 'Invalid one-time password.' });
36+
return cb(null, false, { message: JSON.stringify({ text: 'Invalid one-time password.', otpLength: match.otpMissingLength || 6}) });
37+
}
38+
if (match.otpMissingLength) {
39+
return cb(null, false, { message: JSON.stringify({ text: 'Please enter your one-time password.', otpLength: match.otpMissingLength || 6 })});
4040
}
4141
cb(null, match.matchingUsername);
4242
})
@@ -91,7 +91,7 @@ function authenticate(userToTest, usernameOnly) {
9191
let appsUserHasAccessTo = null;
9292
let matchingUsername = null;
9393
let isReadOnly = false;
94-
let otpMissing = false;
94+
let otpMissingLength = false;
9595
let otpValid = true;
9696

9797
//they provided auth
@@ -104,17 +104,20 @@ function authenticate(userToTest, usernameOnly) {
104104
let usernameMatches = userToTest.name == user.user;
105105
if (usernameMatches && user.mfa && !usernameOnly) {
106106
if (!userToTest.otpCode) {
107-
otpMissing = true;
107+
otpMissingLength = user.mfaDigits || 6;
108108
} else {
109109
const totp = new OTPAuth.TOTP({
110110
algorithm: user.mfaAlgorithm || 'SHA1',
111-
secret: OTPAuth.Secret.fromBase32(user.mfa)
111+
secret: OTPAuth.Secret.fromBase32(user.mfa),
112+
digits: user.mfaDigits,
113+
period: user.mfaPeriod,
112114
});
113115
const valid = totp.validate({
114116
token: userToTest.otpCode
115117
});
116118
if (valid === null) {
117119
otpValid = false;
120+
otpMissingLength = user.mfaDigits || 6;
118121
}
119122
}
120123
}
@@ -132,7 +135,7 @@ function authenticate(userToTest, usernameOnly) {
132135
return {
133136
isAuthenticated,
134137
matchingUsername,
135-
otpMissing,
138+
otpMissingLength,
136139
otpValid,
137140
appsUserHasAccessTo,
138141
isReadOnly,

Parse-Dashboard/CLI/mfa.js

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,19 @@ const generateSecret = ({ app, username, algorithm, digits, period }) => {
6565
secret
6666
});
6767
const url = totp.toString();
68-
return { secret: secret.base32, url };
68+
const config = { mfa: secret.base32 };
69+
config.app = app;
70+
config.url = url;
71+
if (algorithm !== 'SHA1') {
72+
config.mfaAlgorithm = algorithm;
73+
}
74+
if (digits != 6) {
75+
config.mfaDigits = digits;
76+
}
77+
if (period != 30) {
78+
config.mfaPeriod = period;
79+
}
80+
return { config };
6981
};
7082
const showQR = text => {
7183
const QRCode = require('qrcode');
@@ -77,7 +89,10 @@ const showQR = text => {
7789
});
7890
};
7991

80-
const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, config }) => {
92+
const showInstructions = ({ app, username, passwordCopied, encrypt, config }) => {
93+
const {secret, url} = config;
94+
const mfaJSON = {...config};
95+
delete mfaJSON.url;
8196
let orderCounter = 0;
8297
const getOrder = () => {
8398
orderCounter++;
@@ -90,7 +105,7 @@ const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt,
90105

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

96111
if (passwordCopied) {
@@ -101,14 +116,14 @@ const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt,
101116

102117
if (secret) {
103118
console.log(
104-
`\n${getOrder()}. Open the authenticator app to scan the QR code above or enter this secret code:` +
105-
`\n\n ${secret}` +
119+
`\n${getOrder()}. Open the authenticator app to scan the QR code above or enter this secret code:` +
120+
`\n\n ${secret}` +
106121
'\n\n If the secret code generates incorrect one-time passwords, try this alternative:' +
107-
`\n\n ${url}` +
122+
`\n\n ${url}` +
108123
`\n\n${getOrder()}. Destroy any records of the QR code and the secret code to secure the account.`
109124
);
110125
}
111-
126+
112127
if (encrypt) {
113128
console.log(
114129
`\n${getOrder()}. Make sure that "useEncryptedPasswords" is set to "true" in your dashboard configuration.` +
@@ -173,6 +188,7 @@ module.exports = {
173188
const salt = bcrypt.genSaltSync(10);
174189
data.pass = bcrypt.hashSync(data.pass, salt);
175190
}
191+
const config = {};
176192
if (mfa) {
177193
const { app } = await inquirer.prompt([
178194
{
@@ -182,18 +198,13 @@ module.exports = {
182198
}
183199
]);
184200
const { algorithm, digits, period } = await getAlgorithm();
185-
const { secret, url } = generateSecret({ app, username, algorithm, digits, period });
186-
data.mfa = secret;
187-
data.app = app;
188-
data.url = url;
189-
if (algorithm !== 'SHA1') {
190-
data.mfaAlgorithm = algorithm;
191-
}
192-
showQR(data.url);
201+
const secret =generateSecret({ app, username, algorithm, digits, period });
202+
Object.assign(config, secret.config);
203+
showQR(secret.config.url);
193204
}
194-
195-
const config = { mfa: data.mfa, user: data.user, pass: data.pass };
196-
showInstructions({ app: data.app, username, passwordCopied: true, secret: data.mfa, url: data.url, encrypt, config });
205+
config.user = data.user;
206+
config.pass = data.pass ;
207+
showInstructions({ app: data.app, username, passwordCopied: true, encrypt, config });
197208
},
198209
async createMFA() {
199210
console.log('');
@@ -212,14 +223,9 @@ module.exports = {
212223
]);
213224
const { algorithm, digits, period } = await getAlgorithm();
214225

215-
const { url, secret } = generateSecret({ app, username, algorithm, digits, period });
216-
showQR(url);
217-
226+
const { config } = generateSecret({ app, username, algorithm, digits, period });
227+
showQR(config.url);
218228
// Compose config
219-
const config = { mfa: secret };
220-
if (algorithm !== 'SHA1') {
221-
config.mfaAlgorithm = algorithm;
222-
}
223-
showInstructions({ app, username, secret, url, config });
229+
showInstructions({ app, username, config });
224230
}
225231
};

src/lib/tests/Authentication.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jest.dontMock('bcryptjs');
1010

1111
const Authentication = require('../../../Parse-Dashboard/Authentication');
1212
const apps = [{appId: 'test123'}, {appId: 'test789'}];
13-
const readOnlyApps = apps.map((app) => {
13+
const readOnlyApps = apps.map((app) => {
1414
app.readOnly = true;
1515
return app;
1616
});
@@ -55,7 +55,7 @@ function createAuthenticationResult(isAuthenticated, matchingUsername, appsUserH
5555
matchingUsername,
5656
appsUserHasAccessTo,
5757
isReadOnly,
58-
otpMissing: false,
58+
otpMissingLength: false,
5959
otpValid: true
6060
}
6161
}

src/login/Login.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ export default class Login extends React.Component {
1616
super();
1717

1818
let errorDiv = document.getElementById('login_errors');
19+
let otpLength = 6;
1920
if (errorDiv) {
2021
this.errors = errorDiv.innerHTML;
22+
try {
23+
const json = JSON.parse(this.errors)
24+
this.errors = json.text
25+
otpLength = json.otpLength;
26+
} catch (e) {
27+
this.errors = `could not pass error json: ${e}`;
28+
}
2129
}
2230

2331
this.state = {
@@ -30,6 +38,7 @@ export default class Login extends React.Component {
3038
this.inputRefUser = React.createRef();
3139
this.inputRefPass = React.createRef();
3240
this.inputRefMfa = React.createRef();
41+
this.otpLength = otpLength;
3342
}
3443

3544
componentDidMount() {
@@ -53,6 +62,15 @@ export default class Login extends React.Component {
5362
const {path} = this.props;
5463
const updateField = (field, e) => {
5564
this.setState({[field]: e.target.value});
65+
if (field === 'otp' && e.target.value.length >= this.otpLength) {
66+
const input = document.querySelectorAll('input');
67+
for (const field of input) {
68+
if (field.type === 'submit') {
69+
field.click();
70+
break;
71+
}
72+
}
73+
}
5674
}
5775
const formSubmit = () => {
5876
sessionStorage.setItem('username', this.state.username);
@@ -95,7 +113,11 @@ export default class Login extends React.Component {
95113
input={
96114
<input
97115
name='otpCode'
98-
type='number'
116+
type='text'
117+
inputMode="numeric"
118+
autoComplete='one-time-code'
119+
pattern="[0-9]*"
120+
onChange={e => updateField('otp', e)}
99121
ref={this.inputRefMfa}
100122
/>
101123
} />

0 commit comments

Comments
 (0)