Skip to content

Commit a00c4fa

Browse files
authored
fix: MFA recovery codes not consumed after use ([GHSA-4hf6-3x24-c9m8](GHSA-4hf6-3x24-c9m8)) (#10171)
1 parent e092f2c commit a00c4fa

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

spec/AuthenticationAdapters.spec.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,6 +1913,60 @@ describe('OTP TOTP auth adatper', () => {
19131913
);
19141914
});
19151915

1916+
it('consumes recovery code after use', async () => {
1917+
const user = await Parse.User.signUp('username', 'password');
1918+
const OTPAuth = require('otpauth');
1919+
const secret = new OTPAuth.Secret();
1920+
const totp = new OTPAuth.TOTP({
1921+
algorithm: 'SHA1',
1922+
digits: 6,
1923+
period: 30,
1924+
secret,
1925+
});
1926+
const token = totp.generate();
1927+
await user.save(
1928+
{ authData: { mfa: { secret: secret.base32, token } } },
1929+
{ sessionToken: user.getSessionToken() }
1930+
);
1931+
// Get recovery codes from stored auth data
1932+
await user.fetch({ useMasterKey: true });
1933+
const recoveryCode = user.get('authData').mfa.recovery[0];
1934+
// First login with recovery code should succeed
1935+
await request({
1936+
headers,
1937+
method: 'POST',
1938+
url: 'http://localhost:8378/1/login',
1939+
body: JSON.stringify({
1940+
username: 'username',
1941+
password: 'password',
1942+
authData: {
1943+
mfa: {
1944+
token: recoveryCode,
1945+
},
1946+
},
1947+
}),
1948+
});
1949+
// Second login with same recovery code should fail (code consumed)
1950+
await expectAsync(
1951+
request({
1952+
headers,
1953+
method: 'POST',
1954+
url: 'http://localhost:8378/1/login',
1955+
body: JSON.stringify({
1956+
username: 'username',
1957+
password: 'password',
1958+
authData: {
1959+
mfa: {
1960+
token: recoveryCode,
1961+
},
1962+
},
1963+
}),
1964+
}).catch(e => {
1965+
throw e.data;
1966+
})
1967+
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
1968+
});
1969+
19161970
it('future logins reject incorrect TOTP token', async () => {
19171971
const user = await Parse.User.signUp('username', 'password');
19181972
const OTPAuth = require('otpauth');

src/Adapters/Auth/mfa.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
* - Requires a secret key for setup.
4040
* - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
4141
* - Supports configurable digits, period, and algorithm for TOTP generation.
42+
* - Generates two single-use recovery codes during enrollment. Each recovery code can be used once
43+
* in place of a TOTP token and is consumed after use.
4244
*
4345
* ## MFA Payload
4446
* The adapter requires the following `authData` fields:
@@ -157,8 +159,13 @@ class MFAAdapter extends AuthAdapter {
157159
if (!secret) {
158160
return saveResponse;
159161
}
160-
if (recovery[0] === token || recovery[1] === token) {
161-
return saveResponse;
162+
const recoveryIndex = recovery?.indexOf(token) ?? -1;
163+
if (recoveryIndex >= 0) {
164+
const updatedRecovery = [...recovery];
165+
updatedRecovery.splice(recoveryIndex, 1);
166+
return {
167+
save: { ...auth.mfa, recovery: updatedRecovery },
168+
};
162169
}
163170
const totp = new TOTP({
164171
algorithm: this.algorithm,

0 commit comments

Comments
 (0)