Skip to content

Commit a122430

Browse files
committed
task(auth): Have totp/destroy remove recovery phone
Because: - When a user removes 2fa-totp, we want to also destroy any registered recovery phone. - When a user removes 2fa-totp, we want to also destroy an existing backup recovery codes (ie 2fa back up codes). This Commit: - If recovery phone was registered, removes it during call to /totp/destroy - If backup codes exist, removes them during call to /totp/destroy - Adds ability to remove recovery codes to backup-code.manager - Some cleanup: - Renames `recovery-phone` to `account-recovery-phone` in `account/recovery-phone/project.json` to be consistent. - Add tag 'scope:shared:lib' to `account/recovery-phone/project.json` - Add tag 'scope:shared:lib' to `account/recovery-phone/project.json` - Removes entry for `accounts/recovery-phone` in `tsconfig.base.json`, because`@fxa/accounts/recovery-phone` already declared. - Removes entry for `accounts/two-factor` in `tsconfig.base.json`, because `@fxa/accounts/two-factor` already declared. - Changes call in recovery-phone.repository > removePhoneNumber, from `executeTakeFirstOrThrow` to `executeTakeFirst`, which avoids a potentially unhandled error. - Changes `Container.set('BackupCodeManager', backupCodeManager)` to `Container.set(BackupCodeManager, backupCodeManager)`, which is more consistent.
1 parent 1463f62 commit a122430

15 files changed

Lines changed: 124 additions & 123 deletions

File tree

libs/accounts/recovery-phone/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"name": "recovery-phone",
2+
"name": "accounts-recovery-phone",
33
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
44
"sourceRoot": "libs/accounts/recovery-phone/src",
55
"projectType": "library",
6-
"tags": [],
6+
"tags": ["scope:shared:lib"],
77
"targets": {
88
"build": {
99
"executor": "@nx/esbuild:esbuild",

libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
AccountDatabase,
77
AccountDbProvider,
88
testAccountDatabaseSetup,
9+
RecoveryPhoneFactory,
910
} from '@fxa/shared/db/mysql/account';
1011
import { Test } from '@nestjs/testing';
1112

1213
describe('RecoveryPhoneManager', () => {
1314
let recoveryPhoneManager: RecoveryPhoneManager;
1415
let db: AccountDatabase;
16+
const dateMock = jest.spyOn(global.Date, 'now');
1517

1618
// Taken from: https://www.twilio.com/docs/lookup/v2-api#code-lookup-with-data-packages
1719
const mockLookUpData: PhoneNumberLookupData = {
@@ -43,6 +45,7 @@ describe('RecoveryPhoneManager', () => {
4345
};
4446

4547
beforeAll(async () => {
48+
dateMock.mockImplementation(() => 1739227529776);
4649
db = await testAccountDatabaseSetup([
4750
'accounts',
4851
'recoveryPhones',
@@ -67,6 +70,7 @@ describe('RecoveryPhoneManager', () => {
6770

6871
afterAll(async () => {
6972
await db.destroy();
73+
dateMock.mockReset();
7074
});
7175

7276
it('should get a recovery phone', async () => {
@@ -201,6 +205,7 @@ describe('RecoveryPhoneManager', () => {
201205
);
202206

203207
const expectedData = JSON.stringify({
208+
createdAt: 1739227529776,
204209
phoneNumber,
205210
isSetup,
206211
lookupData: JSON.stringify(mockLookUpData),

libs/accounts/recovery-phone/src/lib/recovery-phone.repository.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export async function removePhoneNumber(
2929
const result = await db
3030
.deleteFrom('recoveryPhones')
3131
.where('uid', '=', uid)
32-
.executeTakeFirstOrThrow();
32+
// TBD: Didn't seem like handled an error coming off this call, so
33+
// removed the orThrow...
34+
.executeTakeFirst();
3335

3436
return result.numDeletedRows === BigInt(1);
3537
}

libs/accounts/recovery-phone/src/lib/recovery-phone.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ export class RecoveryPhoneService {
186186
uid
187187
);
188188

189+
// TBD: Random, obs. why do we do this? It seems like a potential edge case, what if the user
190+
// consumes all the recovery codes? This could then return false...
191+
//
192+
// Just curious, about the rationale surrounding why this would block a user from removing their
193+
// phone.
194+
//
189195
if (!hasRecoveryCodes) {
190196
throw new RecoveryNumberRemoveMissingBackupCodes(uid);
191197
}

libs/accounts/two-factor/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
44
"sourceRoot": "libs/accounts/two-factor/src",
55
"projectType": "library",
6-
"tags": [],
6+
"tags": ["scope:shared:lib"],
77
"targets": {
88
"build": {
99
"executor": "@nx/esbuild:esbuild",

libs/accounts/two-factor/src/lib/backup-code.manager.in.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,16 @@ describe('BackupCodeManager', () => {
9292
backupCodeManager.getCountForUserId(getMockUid())
9393
).rejects.toThrow('Database error');
9494
});
95+
96+
it('should delete codes', async () => {
97+
const mockUid1 = getMockUid();
98+
const mockUid2 = getMockUid();
99+
await createRecoveryCode(db, mockUid1);
100+
101+
const result1 = await backupCodeManager.deleteRecoveryCodes(mockUid1);
102+
const result2 = await backupCodeManager.deleteRecoveryCodes(mockUid2);
103+
104+
expect(result1).toBeTruthy();
105+
expect(result2).toBeFalsy();
106+
});
95107
});

libs/accounts/two-factor/src/lib/backup-code.manager.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
AccountDbProvider,
88
} from '@fxa/shared/db/mysql/account';
99
import { Inject, Injectable } from '@nestjs/common';
10-
import { getRecoveryCodes } from './backup-code.repository';
10+
import {
11+
deleteRecoveryCodes,
12+
getRecoveryCodes,
13+
} from './backup-code.repository';
1114

1215
@Injectable()
1316
export class BackupCodeManager {
@@ -34,4 +37,15 @@ export class BackupCodeManager {
3437
count: recoveryCodes.length,
3538
};
3639
}
40+
41+
/**
42+
* Removes recover codes for a given uid.
43+
*
44+
* @param uid - The uid in hexadecimal string format.
45+
* @returns True if one or more codes were removed
46+
*/
47+
async deleteRecoveryCodes(uid: string) {
48+
const success = await deleteRecoveryCodes(this.db, Buffer.from(uid, 'hex'));
49+
return success;
50+
}
3751
}

libs/accounts/two-factor/src/lib/backup-code.repository.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ export async function getRecoveryCodes(db: AccountDatabase, uid: Buffer) {
1010
.selectAll()
1111
.execute();
1212
}
13+
14+
export async function deleteRecoveryCodes(db: AccountDatabase, uid: Buffer) {
15+
const result = await db
16+
.deleteFrom('recoveryCodes')
17+
.where('uid', '=', uid)
18+
.executeTakeFirst();
19+
20+
return result.numDeletedRows === BigInt(1);
21+
}

libs/shared/db/mysql/account/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
AccountFactory,
1010
AccountCustomerFactory,
1111
PaypalCustomerFactory,
12+
RecoveryPhoneFactory,
1213
} from './lib/factories';
1314
export { setupAccountDatabase, AccountDbProvider } from './lib/setup';
1415
export { testAccountDatabaseSetup } from './lib/tests';

packages/fxa-auth-server/bin/key_server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async function run(config) {
130130
Container.set(AccountManager, accountManager);
131131

132132
const backupCodeManager = new BackupCodeManager(accountDatabase);
133-
Container.set('BackupCodeManager', backupCodeManager);
133+
Container.set(BackupCodeManager, backupCodeManager);
134134

135135
// Set currencyHelper before stripe and paypal helpers, so they can use it.
136136
try {

0 commit comments

Comments
 (0)