Skip to content

Commit e61af4f

Browse files
authored
test: Stale MFA unlink bypass via skipped adapter validation (#10277)
1 parent e521eb2 commit e61af4f

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

spec/AuthenticationAdapters.spec.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,82 @@ describe('OTP TOTP auth adatper', () => {
20012001
})
20022002
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
20032003
});
2004+
2005+
it('allows unlinking MFA without TOTP verification (by design)', async () => {
2006+
const user = await Parse.User.signUp('username', 'password');
2007+
const sessionToken = user.getSessionToken();
2008+
const OTPAuth = require('otpauth');
2009+
const secret = new OTPAuth.Secret();
2010+
const totp = new OTPAuth.TOTP({
2011+
algorithm: 'SHA1',
2012+
digits: 6,
2013+
period: 30,
2014+
secret,
2015+
});
2016+
const token = totp.generate();
2017+
// Enable MFA
2018+
await user.save(
2019+
{ authData: { mfa: { secret: secret.base32, token } } },
2020+
{ sessionToken }
2021+
);
2022+
await user.fetch({ useMasterKey: true });
2023+
expect(user.get('authData').mfa.secret).toBeDefined();
2024+
// Unlink MFA without providing TOTP
2025+
await user.save(
2026+
{ authData: { mfa: null } },
2027+
{ sessionToken }
2028+
);
2029+
// MFA should be removed
2030+
await user.fetch({ useMasterKey: true });
2031+
expect(user.get('authData')).toBeUndefined();
2032+
// Login should succeed without MFA
2033+
const response = await request({
2034+
headers,
2035+
method: 'POST',
2036+
url: 'http://localhost:8378/1/login',
2037+
body: JSON.stringify({
2038+
username: 'username',
2039+
password: 'password',
2040+
}),
2041+
});
2042+
expect(response.data.sessionToken).toBeDefined();
2043+
});
2044+
2045+
it('allows blocking MFA unlink via beforeSave trigger', async () => {
2046+
Parse.Cloud.beforeSave('_User', request => {
2047+
const authData = request.object.get('authData');
2048+
if (authData?.mfa === null) {
2049+
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Cannot disable MFA without verification');
2050+
}
2051+
});
2052+
const user = await Parse.User.signUp('username', 'password');
2053+
const OTPAuth = require('otpauth');
2054+
const secret = new OTPAuth.Secret();
2055+
const totp = new OTPAuth.TOTP({
2056+
algorithm: 'SHA1',
2057+
digits: 6,
2058+
period: 30,
2059+
secret,
2060+
});
2061+
const token = totp.generate();
2062+
// Enable MFA
2063+
await user.save(
2064+
{ authData: { mfa: { secret: secret.base32, token } } },
2065+
{ sessionToken: user.getSessionToken() }
2066+
);
2067+
// Attempt to unlink MFA — should be blocked by beforeSave trigger
2068+
await expectAsync(
2069+
user.save(
2070+
{ authData: { mfa: null } },
2071+
{ sessionToken: user.getSessionToken() }
2072+
)
2073+
).toBeRejectedWith(
2074+
new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Cannot disable MFA without verification')
2075+
);
2076+
// MFA should still be enabled
2077+
await user.fetch({ useMasterKey: true });
2078+
expect(user.get('authData').mfa.secret).toBeDefined();
2079+
});
20042080
});
20052081

20062082
describe('OTP SMS auth adatper', () => {

0 commit comments

Comments
 (0)