Skip to content

Commit 8cab0e5

Browse files
authored
Merge pull request #19486 from mozilla/FXA-12425
bug(auth, tests): Issue AAL2 token for password reset with recovery key
2 parents 74a55a7 + 393eba8 commit 8cab0e5

3 files changed

Lines changed: 121 additions & 2 deletions

File tree

packages/functional-tests/tests/resetPassword/resetPassword2FA.spec.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ test.describe('severity-1 #smoke', () => {
116116
credentials.password = newPassword;
117117
});
118118

119-
test('can reset password with recovery key and 2FA enabled but not prompted', async ({
119+
test('can reset password with recovery key without 2FA prompt', async ({
120120
target,
121121
pages: { signin, resetPassword, settings, totp, recoveryKey },
122122
testAccountTracker,
@@ -183,6 +183,82 @@ test.describe('severity-1 #smoke', () => {
183183
credentials.password = newPassword;
184184
});
185185

186+
test('can reset password with recovery key then delete account', async ({
187+
target,
188+
pages: {
189+
page,
190+
signin,
191+
resetPassword,
192+
settings,
193+
totp,
194+
recoveryKey,
195+
deleteAccount,
196+
},
197+
testAccountTracker,
198+
}) => {
199+
const credentials = await testAccountTracker.signUp();
200+
const newPassword = testAccountTracker.generatePassword();
201+
202+
await signin.goto();
203+
await signin.fillOutEmailFirstForm(credentials.email);
204+
await signin.fillOutPasswordForm(credentials.password);
205+
206+
// Enable 2FA
207+
await expect(settings.settingsHeading).toBeVisible();
208+
await expect(settings.totp.status).toHaveText('Disabled');
209+
210+
await settings.totp.addButton.click();
211+
await totp.setUpTwoStepAuthWithQrAndBackupCodesChoice();
212+
213+
await expect(settings.settingsHeading).toBeVisible();
214+
await expect(settings.alertBar).toHaveText(
215+
'Two-step authentication has been enabled'
216+
);
217+
await expect(settings.totp.status).toHaveText('Enabled');
218+
219+
// Create recovery key
220+
await settings.recoveryKey.createButton.click();
221+
const key = await recoveryKey.createRecoveryKey(
222+
credentials.password,
223+
'hint'
224+
);
225+
226+
// Verify status as 'enabled'
227+
await expect(settings.settingsHeading).toBeVisible();
228+
await expect(settings.recoveryKey.status).toHaveText('Enabled');
229+
230+
await settings.signOut();
231+
232+
await resetPassword.goto();
233+
234+
await resetPassword.fillOutEmailForm(credentials.email);
235+
236+
const code = await target.emailClient.getResetPasswordCode(
237+
credentials.email
238+
);
239+
await resetPassword.fillOutResetPasswordCodeForm(code);
240+
241+
// Not prompted for 2FA during reset password
242+
await resetPassword.fillOutRecoveryKeyForm(key);
243+
await resetPassword.fillOutNewPasswordForm(newPassword);
244+
245+
await expect(resetPassword.passwordResetPasswordSaved).toBeVisible();
246+
247+
await resetPassword.continueWithoutDownloadingRecoveryKey();
248+
await resetPassword.recoveryKeyFinishButton.click();
249+
250+
await expect(settings.settingsHeading).toBeVisible();
251+
252+
// Recovery key has been consumed and a new one created
253+
await expect(settings.recoveryKey.status).toHaveText('Enabled');
254+
255+
// Account deletion requires AAL match
256+
// If AAL does not match, account deletion fails with 'unconfirmed session' error
257+
await settings.deleteAccountButton.click();
258+
await deleteAccount.deleteAccount(newPassword);
259+
await expect(page.getByText('Account deleted successfully')).toBeVisible();
260+
});
261+
186262
test('can reset password with 2FA and forgot recovery key', async ({
187263
page,
188264
target,

packages/fxa-auth-server/lib/routes/account.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,7 @@ export class AccountHandler {
10441044
// they have to verify the token.
10451045
const hasTotpToken = await this.otpUtils.hasTotpToken(accountRecord);
10461046
if (hasTotpToken) {
1047-
// User has enabled TOTP, no way around it, they must verify TOTP token
1047+
// User has enabled TOTP, they must verify TOTP token unless they are using recovery key
10481048
verificationMethod = 'totp-2fa';
10491049
} else if (!hasTotpToken && verificationMethod === 'totp-2fa') {
10501050
// Error if requesting TOTP verification with TOTP not setup
@@ -1766,6 +1766,12 @@ export class AccountHandler {
17661766
sessionToken.id,
17671767
accountResetToken.verificationMethod
17681768
);
1769+
} else if (recoveryKeyId && hasTotpToken) {
1770+
// If account has TOTP but recovery key is used,
1771+
// we elevate to AAL2 to match account AAL.
1772+
// Recovery key is considered a 2FA method.
1773+
// Only elevate if account has TOTP, otherwise session AAL > account AAL.
1774+
await this.db.verifyTokensWithMethod(sessionToken.id, 'totp-2fa');
17691775
}
17701776

17711777
return await request.propagateMetricsContext(

packages/fxa-auth-server/test/local/routes/account.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,43 @@ describe('/account/reset', () => {
439439
null,
440440
'db.createSessionToken was passed correct form factor'
441441
);
442+
443+
// Token is not verified with TOTP-2FA method (AAL2) if account does not have TOTP
444+
assert.equal(
445+
mockDB.verifyTokensWithMethod.callCount,
446+
0,
447+
'db.verifyTokensWithMethod was not called'
448+
);
449+
});
450+
});
451+
452+
describe('reset account with account recovery key, TOTP enabled', () => {
453+
let res;
454+
beforeEach(() => {
455+
mockDB.totpToken = sinon.spy(() => {
456+
return Promise.resolve({
457+
verified: true,
458+
enabled: true,
459+
});
460+
});
461+
mockRequest.payload.wrapKb = hexString(32);
462+
mockRequest.payload.recoveryKeyId = hexString(16);
463+
return runTest(route, mockRequest, (result) => (res = result));
464+
});
465+
466+
it('should return response', () => {
467+
assert.ok(res.sessionToken, 'return sessionToken');
468+
assert.ok(res.keyFetchToken, 'return keyFetchToken');
469+
});
470+
471+
it('should verify token with TOTP-2FA method (AAL2) if account has TOTP', () => {
472+
assert.equal(
473+
mockDB.verifyTokensWithMethod.callCount,
474+
1,
475+
'db.verifyTokensWithMethod was called once'
476+
);
477+
const verifyArgs = mockDB.verifyTokensWithMethod.args[0];
478+
assert.equal(verifyArgs[1], 'totp-2fa');
442479
});
443480
});
444481

0 commit comments

Comments
 (0)