refactor: Change response types of TOTP adapter to match existing adapters (#8661)

This commit is contained in:
Daniel
2023-07-07 01:22:18 +10:00
committed by GitHub
parent 02f40fd896
commit c9b59719ec
2 changed files with 63 additions and 18 deletions

View File

@@ -2445,9 +2445,9 @@ describe('OTP TOTP auth adatper', () => {
const response = user.get('authDataResponse'); const response = user.get('authDataResponse');
expect(response.mfa).toBeDefined(); expect(response.mfa).toBeDefined();
expect(response.mfa.recovery).toBeDefined(); expect(response.mfa.recovery).toBeDefined();
expect(response.mfa.recovery.length).toEqual(2); expect(response.mfa.recovery.split(',').length).toEqual(2);
await user.fetch(); await user.fetch();
expect(user.get('authData').mfa).toEqual({ enabled: true }); expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
}); });
it('can login with valid token', async () => { it('can login with valid token', async () => {
@@ -2473,13 +2473,15 @@ describe('OTP TOTP auth adatper', () => {
username: 'username', username: 'username',
password: 'password', password: 'password',
authData: { authData: {
mfa: totp.generate(), mfa: {
token: totp.generate(),
},
}, },
}), }),
}).then(res => res.data); }).then(res => res.data);
expect(response.objectId).toEqual(user.id); expect(response.objectId).toEqual(user.id);
expect(response.sessionToken).toBeDefined(); expect(response.sessionToken).toBeDefined();
expect(response.authData).toEqual({ mfa: { enabled: true } }); expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
expect(Object.keys(response).sort()).toEqual( expect(Object.keys(response).sort()).toEqual(
[ [
'objectId', 'objectId',
@@ -2528,6 +2530,42 @@ describe('OTP TOTP auth adatper', () => {
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32); expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
}); });
it('cannot change OTP with invalid token', 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() }
);
const new_secret = new OTPAuth.Secret();
const new_totp = new OTPAuth.TOTP({
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: new_secret,
});
const new_token = new_totp.generate();
await expectAsync(
user.save(
{
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
},
{ sessionToken: user.getSessionToken() }
)
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
await user.fetch({ useMasterKey: true });
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
});
it('future logins require TOTP token', async () => { it('future logins require TOTP token', async () => {
const user = await Parse.User.signUp('username', 'password'); const user = await Parse.User.signUp('username', 'password');
const OTPAuth = require('otpauth'); const OTPAuth = require('otpauth');
@@ -2572,7 +2610,9 @@ describe('OTP TOTP auth adatper', () => {
username: 'username', username: 'username',
password: 'password', password: 'password',
authData: { authData: {
mfa: 'abcd', mfa: {
token: 'abcd',
},
}, },
}), }),
}).catch(e => { }).catch(e => {
@@ -2619,7 +2659,7 @@ describe('OTP SMS auth adatper', () => {
const spy = spyOn(mfa, 'sendSMS').and.callThrough(); const spy = spyOn(mfa, 'sendSMS').and.callThrough();
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken }); await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
await user.fetch({ sessionToken }); await user.fetch({ sessionToken });
expect(user.get('authData')).toEqual({ mfa: { enabled: false } }); expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
expect(spy).toHaveBeenCalledWith(code, '+11111111111'); expect(spy).toHaveBeenCalledWith(code, '+11111111111');
await user.fetch({ useMasterKey: true }); await user.fetch({ useMasterKey: true });
const authData = user.get('authData').mfa?.pending; const authData = user.get('authData').mfa?.pending;
@@ -2629,7 +2669,7 @@ describe('OTP SMS auth adatper', () => {
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken }); await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
await user.fetch({ sessionToken }); await user.fetch({ sessionToken });
expect(user.get('authData')).toEqual({ mfa: { enabled: true } }); expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
}); });
it('future logins require SMS code', async () => { it('future logins require SMS code', async () => {
@@ -2658,7 +2698,9 @@ describe('OTP SMS auth adatper', () => {
username: 'username', username: 'username',
password: 'password', password: 'password',
authData: { authData: {
mfa: true, mfa: {
token: 'request',
},
}, },
}), }),
}).catch(e => e.data); }).catch(e => e.data);
@@ -2672,13 +2714,15 @@ describe('OTP SMS auth adatper', () => {
username: 'username', username: 'username',
password: 'password', password: 'password',
authData: { authData: {
mfa: code, mfa: {
token: code,
},
}, },
}), }),
}).then(res => res.data); }).then(res => res.data);
expect(response.objectId).toEqual(user.id); expect(response.objectId).toEqual(user.id);
expect(response.sessionToken).toBeDefined(); expect(response.sessionToken).toBeDefined();
expect(response.authData).toEqual({ mfa: { enabled: true } }); expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
expect(Object.keys(response).sort()).toEqual( expect(Object.keys(response).sort()).toEqual(
[ [
'objectId', 'objectId',

View File

@@ -44,14 +44,15 @@ class MFAAdapter extends AuthAdapter {
} }
throw 'Invalid MFA data'; throw 'Invalid MFA data';
} }
async validateLogin(token, _, req) { async validateLogin(loginData, _, req) {
const saveResponse = { const saveResponse = {
doNotSave: true, doNotSave: true,
}; };
const token = loginData.token;
const auth = req.original.get('authData') || {}; const auth = req.original.get('authData') || {};
const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {}; const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {};
if (this.sms && mobile) { if (this.sms && mobile) {
if (typeof token === 'boolean') { if (token === 'request') {
const { token: sendToken, expiry } = await this.sendSMS(mobile); const { token: sendToken, expiry } = await this.sendSMS(mobile);
auth.mfa.token = sendToken; auth.mfa.token = sendToken;
auth.mfa.expiry = expiry; auth.mfa.expiry = expiry;
@@ -96,7 +97,7 @@ class MFAAdapter extends AuthAdapter {
} }
return saveResponse; return saveResponse;
} }
validateUpdate(authData, _, req) { async validateUpdate(authData, _, req) {
if (req.master) { if (req.master) {
return; return;
} }
@@ -107,7 +108,7 @@ class MFAAdapter extends AuthAdapter {
return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {}); return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {});
} }
if (this.totp) { if (this.totp) {
this.validateLogin(authData.old, null, req); await this.validateLogin({ token: authData.old }, null, req);
return this.validateSetUp(authData); return this.validateSetUp(authData);
} }
throw 'Invalid MFA data'; throw 'Invalid MFA data';
@@ -118,16 +119,16 @@ class MFAAdapter extends AuthAdapter {
} }
if (this.totp && authData.secret) { if (this.totp && authData.secret) {
return { return {
enabled: true, status: 'enabled',
}; };
} }
if (this.sms && authData.mobile) { if (this.sms && authData.mobile) {
return { return {
enabled: true, status: 'enabled',
}; };
} }
return { return {
enabled: false, status: 'disabled',
}; };
} }
@@ -204,7 +205,7 @@ class MFAAdapter extends AuthAdapter {
} }
const recovery = [randomString(30), randomString(30)]; const recovery = [randomString(30), randomString(30)];
return { return {
response: { recovery }, response: { recovery: recovery.join(', ') },
save: { secret, recovery }, save: { secret, recovery },
}; };
} }