Skip to content

Commit cda5084

Browse files
authored
Merge pull request #18337 from mozilla/FXA-11025
task(auth): Have totp/destroy remove recovery phone
2 parents 67558a8 + a122430 commit cda5084

14 files changed

Lines changed: 123 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,13 +6,15 @@ import {
66
AccountDatabase,
77
AccountDbProvider,
88
testAccountDatabaseSetup,
9+
RecoveryPhoneFactory,
910
} from '@fxa/shared/db/mysql/account';
1011
import { Test } from '@nestjs/testing';
1112
import { RecoveryPhoneFactory } from '@fxa/shared/db/mysql/account';
1213

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

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

4648
beforeAll(async () => {
49+
dateMock.mockImplementation(() => 1739227529776);
4750
db = await testAccountDatabaseSetup([
4851
'accounts',
4952
'recoveryPhones',
@@ -68,6 +71,7 @@ describe('RecoveryPhoneManager', () => {
6871

6972
afterAll(async () => {
7073
await db.destroy();
74+
dateMock.mockReset();
7175
});
7276

7377
it('should get a recovery phone', async () => {
@@ -202,6 +206,7 @@ describe('RecoveryPhoneManager', () => {
202206
);
203207

204208
const expectedData = JSON.stringify({
209+
createdAt: 1739227529776,
205210
phoneNumber,
206211
isSetup,
207212
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
@@ -198,6 +198,12 @@ export class RecoveryPhoneService {
198198
uid
199199
);
200200

201+
// TBD: Random, obs. why do we do this? It seems like a potential edge case, what if the user
202+
// consumes all the recovery codes? This could then return false...
203+
//
204+
// Just curious, about the rationale surrounding why this would block a user from removing their
205+
// phone.
206+
//
201207
if (!hasRecoveryCodes) {
202208
throw new RecoveryNumberRemoveMissingBackupCodes(uid);
203209
}

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+
}

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 {

packages/fxa-auth-server/lib/routes/recovery-codes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ const validators = require('./validators');
1010
const { Container } = require('typedi');
1111
const RECOVERY_CODES_DOCS =
1212
require('../../docs/swagger/recovery-codes-api').default;
13+
const { BackupCodeManager } = require('@fxa/accounts/two-factor');
1314

1415
const RECOVERY_CODE_SANE_MAX_LENGTH = 20;
1516

1617
module.exports = (log, db, config, customs, mailer, glean) => {
1718
const codeConfig = config.recoveryCodes;
1819
const RECOVERY_CODE_COUNT = (codeConfig && codeConfig.count) || 8;
19-
const backupCodeManager = Container.get('BackupCodeManager');
20+
const backupCodeManager = Container.get(BackupCodeManager);
2021

2122
// Validate backup authentication codes
2223
const recoveryCodesSchema = validators.recoveryCodes(

0 commit comments

Comments
 (0)