mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: MFA recovery codes not consumed after use ([GHSA-4hf6-3x24-c9m8](https://github.com/parse-community/parse-server/security/advisories/GHSA-4hf6-3x24-c9m8)) (#10170)
This commit is contained in:
@@ -1913,6 +1913,60 @@ describe('OTP TOTP auth adatper', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('consumes recovery code after use', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
// Get recovery codes from stored auth data
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const recoveryCode = user.get('authData').mfa.recovery[0];
|
||||
// First login with recovery code should succeed
|
||||
await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: recoveryCode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
// Second login with same recovery code should fail (code consumed)
|
||||
await expectAsync(
|
||||
request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: recoveryCode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw e.data;
|
||||
})
|
||||
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
|
||||
});
|
||||
|
||||
it('future logins reject incorrect TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
* - Requires a secret key for setup.
|
||||
* - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
|
||||
* - Supports configurable digits, period, and algorithm for TOTP generation.
|
||||
* - Generates two single-use recovery codes during enrollment. Each recovery code can be used once
|
||||
* in place of a TOTP token and is consumed after use.
|
||||
*
|
||||
* ## MFA Payload
|
||||
* The adapter requires the following `authData` fields:
|
||||
@@ -157,8 +159,13 @@ class MFAAdapter extends AuthAdapter {
|
||||
if (!secret) {
|
||||
return saveResponse;
|
||||
}
|
||||
if (recovery[0] === token || recovery[1] === token) {
|
||||
return saveResponse;
|
||||
const recoveryIndex = recovery?.indexOf(token) ?? -1;
|
||||
if (recoveryIndex >= 0) {
|
||||
const updatedRecovery = [...recovery];
|
||||
updatedRecovery.splice(recoveryIndex, 1);
|
||||
return {
|
||||
save: { ...auth.mfa, recovery: updatedRecovery },
|
||||
};
|
||||
}
|
||||
const totp = new TOTP({
|
||||
algorithm: this.algorithm,
|
||||
|
||||
Reference in New Issue
Block a user