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:
Manuel
2026-03-10 04:22:17 +00:00
committed by GitHub
parent 620844d00a
commit 18abdd960b
2 changed files with 63 additions and 2 deletions
+54
View File
@@ -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');
+9 -2
View File
@@ -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,