Skip to content

Commit 904fa79

Browse files
committed
feat(password): Don't revoke recovery key when doing a password change
1 parent 05e05f4 commit 904fa79

8 files changed

Lines changed: 156 additions & 7 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
2+
3+
CALL assertPatchLevel('159');
4+
5+
CREATE PROCEDURE `resetAccount_19` (
6+
IN `uidArg` BINARY(16),
7+
IN `verifyHashArg` BINARY(32),
8+
IN `verifyHashVersion2Arg` BINARY(32),
9+
IN `authSaltArg` BINARY(32),
10+
IN `clientSaltArg` VARCHAR(128),
11+
IN `wrapWrapKbArg` BINARY(32),
12+
IN `wrapWrapKbVersion2Arg` BINARY(32),
13+
IN `verifierSetAtArg` BIGINT UNSIGNED,
14+
IN `verifierVersionArg` TINYINT UNSIGNED,
15+
IN `keysHaveChangedArg` BOOLEAN,
16+
IN `isPasswordUpgrade` BOOLEAN
17+
)
18+
BEGIN
19+
DECLARE EXIT HANDLER FOR SQLEXCEPTION
20+
BEGIN
21+
ROLLBACK;
22+
RESIGNAL;
23+
END;
24+
25+
START TRANSACTION;
26+
27+
-- When we upgrade accounts to key stretching v2, we do
28+
-- an 'automated reset' for the user. When we do this, we
29+
-- preserve the underlying private key, so there's actually
30+
-- no reason to drop associated data.
31+
IF isPasswordUpgrade = 0 THEN
32+
DELETE FROM sessionTokens WHERE uid = uidArg;
33+
DELETE FROM keyFetchTokens WHERE uid = uidArg;
34+
DELETE FROM accountResetTokens WHERE uid = uidArg;
35+
DELETE FROM passwordChangeTokens WHERE uid = uidArg;
36+
DELETE FROM passwordForgotTokens WHERE uid = uidArg;
37+
DELETE devices, deviceCommands FROM devices LEFT JOIN deviceCommands
38+
ON (deviceCommands.uid = devices.uid AND deviceCommands.deviceId = devices.id)
39+
WHERE devices.uid = uidArg; DELETE FROM unverifiedTokens WHERE uid = uidArg;
40+
END IF;
41+
42+
-- We should only revoke the recovery key if the users private key has changed.
43+
IF keysHaveChangedArg = 1 THEN
44+
DELETE FROM recoveryKeys WHERE uid = uidArg;
45+
END IF;
46+
47+
UPDATE accounts
48+
SET
49+
verifyHash = verifyHashArg,
50+
verifyHashVersion2 = verifyHashVersion2Arg,
51+
wrapWrapKb = wrapWrapKbArg,
52+
wrapWrapKbVersion2 = wrapWrapKbVersion2Arg,
53+
authSalt = authSaltArg,
54+
clientSalt = clientSaltArg,
55+
verifierVersion = verifierVersionArg,
56+
profileChangedAt = verifierSetAtArg,
57+
-- The `keysChangedAt` column was added in a migration, so its default value
58+
-- is NULL meaning "we don't know". Now that we do know whether or not the keys
59+
-- are being changed, ensure it gets set to some concrete non-NULL value.
60+
keysChangedAt = IF(keysHaveChangedArg, verifierSetAtArg, COALESCE(keysChangedAt, verifierSetAt, createdAt)),
61+
verifierSetAt = verifierSetAtArg,
62+
lockedAt = NULL
63+
WHERE uid = uidArg;
64+
65+
COMMIT;
66+
END;
67+
68+
UPDATE dbMetadata SET value = '160' WHERE name = 'schema-patch-level';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- DROP PROCEDURE `CREATE PROCEDURE `resetAccount_19`;
2+
3+
-- UPDATE dbMetadata SET value = '159' WHERE name = 'schema-patch-level';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"level": 159
2+
"level": 160
33
}

packages/functional-tests/pages/resetPassword.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,8 @@ export class ResetPasswordPage extends BaseLayout {
218218
]);
219219
return download;
220220
}
221+
222+
async continueWithoutDownloadingRecoveryKey() {
223+
return this.page.getByText('Continue without downloading').click();
224+
}
221225
}

packages/functional-tests/tests/misc/recoveryKeyPromoInline.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ test.describe('recovery key promo', () => {
4343

4444
test('not shown if user already has a recovery key', async ({
4545
target,
46-
pages: { page, signin, connectAnotherDevice, settings, recoveryKey },
46+
syncBrowserPages: {
47+
page,
48+
signin,
49+
connectAnotherDevice,
50+
settings,
51+
recoveryKey,
52+
},
4753
testAccountTracker,
4854
}) => {
4955
const credentials = await testAccountTracker.signUp();

packages/functional-tests/tests/misc/recoveryKeyPromoSettingsBanner.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ test.describe('recovery key promo', () => {
88
test.describe('settings banner', () => {
99
test('can setup recovery key from settings promo banner', async ({
1010
target,
11-
pages: { page, inlineRecoveryKey, signin, settings, recoveryKey },
11+
syncBrowserPages: {
12+
page,
13+
inlineRecoveryKey,
14+
signin,
15+
settings,
16+
recoveryKey,
17+
},
1218
testAccountTracker,
1319
}) => {
1420
const credentials = await testAccountTracker.signUp();
@@ -32,7 +38,13 @@ test.describe('recovery key promo', () => {
3238

3339
test('can dismiss', async ({
3440
target,
35-
pages: { page, inlineRecoveryKey, signin, settings, recoveryKey },
41+
syncBrowserPages: {
42+
page,
43+
inlineRecoveryKey,
44+
signin,
45+
settings,
46+
recoveryKey,
47+
},
3648
testAccountTracker,
3749
}) => {
3850
const credentials = await testAccountTracker.signUp();

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ test.describe('severity-1 #smoke', () => {
3131
settings
3232
);
3333

34+
await settings.signOut();
35+
3436
// Stash original encryption keys to be verified later
3537
const accountData = await target.authClient.sessionReauth(
3638
credentials.sessionToken,
@@ -127,6 +129,8 @@ test.describe('severity-1 #smoke', () => {
127129

128130
await enableRecoveryKey(credentials.password, recoveryKey, settings);
129131

132+
await settings.signOut();
133+
130134
await resetPassword.goto();
131135

132136
await resetPassword.fillOutEmailForm(credentials.email);
@@ -149,6 +153,60 @@ test.describe('severity-1 #smoke', () => {
149153
// update password for cleanup function
150154
credentials.password = newPassword;
151155
});
156+
157+
test('can reset password with recovery key after changing password in settings', async ({
158+
target,
159+
pages: { settings, recoveryKey, resetPassword, signin, changePassword },
160+
testAccountTracker,
161+
}) => {
162+
const credentials = await testAccountTracker.signUp();
163+
const newPassword = testAccountTracker.generatePassword();
164+
165+
await signinAccount(
166+
credentials.email,
167+
credentials.password,
168+
settings,
169+
signin
170+
);
171+
172+
const key = await enableRecoveryKey(
173+
credentials.password,
174+
recoveryKey,
175+
settings
176+
);
177+
178+
// Change password
179+
await settings.password.changeButton.click();
180+
await changePassword.fillOutChangePassword(
181+
credentials.password,
182+
newPassword
183+
);
184+
185+
await expect(settings.settingsHeading).toBeVisible();
186+
await expect(settings.alertBar).toHaveText('Password updated');
187+
188+
await resetPassword.goto();
189+
190+
await resetPassword.fillOutEmailForm(credentials.email);
191+
const code = await target.emailClient.getResetPasswordCode(
192+
credentials.email
193+
);
194+
195+
await resetPassword.fillOutResetPasswordCodeForm(code);
196+
197+
await resetPassword.fillOutRecoveryKeyForm(key);
198+
await resetPassword.fillOutNewPasswordForm(newPassword);
199+
200+
await resetPassword.continueWithoutDownloadingRecoveryKey();
201+
202+
await resetPassword.recoveryKeyHintTextbox.fill('area 51');
203+
await resetPassword.recoveryKeyFinishButton.click();
204+
205+
await expect(settings.recoveryKey.status).toHaveText('Enabled');
206+
207+
// update password for cleanup function
208+
credentials.password = newPassword;
209+
});
152210
});
153211

154212
async function signinAccount(
@@ -177,8 +235,6 @@ test.describe('severity-1 #smoke', () => {
177235
await expect(settings.settingsHeading).toBeVisible();
178236
await expect(settings.recoveryKey.status).toHaveText('Enabled');
179237

180-
await settings.signOut();
181-
182238
return key;
183239
}
184240
});

packages/fxa-shared/db/models/auth/base-auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export enum Proc {
5252
Prune = 'prune_10',
5353
RecoveryCodes = 'recoveryCodes_1',
5454
RecoveryKey = 'getRecoveryKey_4',
55-
ResetAccount = 'resetAccount_18',
55+
ResetAccount = 'resetAccount_19',
5656
ResetAccountTokens = 'resetAccountTokens_1',
5757
SessionWithDevice = 'sessionWithDevice_19',
5858
Sessions = 'sessions_13',

0 commit comments

Comments
 (0)