Skip to content

Commit

Permalink
feat(2fa): Add backup-code libs
Browse files Browse the repository at this point in the history
  • Loading branch information
vbudhram committed Nov 1, 2024
1 parent 88d94a6 commit e123ab9
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 1 deletion.
23 changes: 23 additions & 0 deletions libs/accounts/two-factor/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {}
}
]
}
11 changes: 11 additions & 0 deletions libs/accounts/two-factor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# accounts-two-factor

This library was generated with [Nx](https://nx.dev).

## Building

Run `nx build accounts-two-factor` to build the library.

## Running unit tests

Run `nx test accounts-two-factor` to execute the unit tests via [Jest](https://jestjs.io).
11 changes: 11 additions & 0 deletions libs/accounts/two-factor/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'accounts-two-factor',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/accounts/two-factor',
};
4 changes: 4 additions & 0 deletions libs/accounts/two-factor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "accounts/two-factor",
"version": "0.0.1"
}
27 changes: 27 additions & 0 deletions libs/accounts/two-factor/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "accounts-two-factor",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/accounts/two-factor/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/accounts/two-factor",
"tsConfig": "libs/accounts/two-factor/tsconfig.lib.json",
"packageJson": "libs/accounts/two-factor/package.json",
"main": "libs/accounts/two-factor/src/index.ts",
"assets": ["libs/accounts/two-factor/*.md"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/accounts/two-factor/jest.config.ts"
}
}
}
}
1 change: 1 addition & 0 deletions libs/accounts/two-factor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/backup-code.manager';
41 changes: 41 additions & 0 deletions libs/accounts/two-factor/src/lib/backup-code.factories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { faker } from '@faker-js/faker';

interface RecoveryCode {
uid: Buffer;
codeHash: Buffer;
salt: Buffer;
}

export const RecoveryCodeFactory = (
override?: Partial<RecoveryCode>
): RecoveryCode => ({
uid: Buffer.from(
faker.string.hexadecimal({
length: 32,
prefix: '',
casing: 'lower',
}),
'hex'
),
codeHash: Buffer.from(
faker.string.hexadecimal({
length: 32,
prefix: '',
casing: 'lower',
}),
'hex'
),
salt: Buffer.from(
faker.string.hexadecimal({
length: 32,
prefix: '',
casing: 'lower',
}),
'hex'
),
...override,
});
83 changes: 83 additions & 0 deletions libs/accounts/two-factor/src/lib/backup-code.manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { BackupCodeManager } from './backup-code.manager';
import {
AccountDatabase,
testAccountDatabaseSetup,
} from '@fxa/shared/db/mysql/account';
import { RecoveryCodeFactory } from './backup-code.factories';
import { faker } from '@faker-js/faker';

function getMockUid() {
return faker.string.hexadecimal({
length: 32,
prefix: '',
casing: 'lower',
});
}

describe('BackupCodeManager', () => {
let backupCodeManager: BackupCodeManager;
let db: AccountDatabase;

async function createRecoveryCode(db: AccountDatabase, uid: string) {
return db
.insertInto('recoveryCodes')
.values({
...RecoveryCodeFactory({
uid: Buffer.from(uid, 'hex'),
}),
})
.execute();
}

beforeEach(async () => {
db = await testAccountDatabaseSetup(['accounts', 'recoveryCodes']);
backupCodeManager = new BackupCodeManager(db);
});

afterAll(async () => {
await db.destroy();
});

it('should return that the user has backup codes and count them', async () => {
const mockUid = getMockUid();
await createRecoveryCode(db, mockUid);
await createRecoveryCode(db, mockUid);
await createRecoveryCode(db, mockUid);

const result = await backupCodeManager.getCountForUserId(mockUid);
expect(result.hasBackupCodes).toBe(true);
expect(result.count).toBe(3);
});

it('should return that the user has no backup codes', async () => {
const result = await backupCodeManager.getCountForUserId('abcd');
expect(result.hasBackupCodes).toBe(false);
expect(result.count).toBe(0);
});

it('should handle multiple users with different backup code counts', async () => {
const mockUid1 = getMockUid();
const mockUid2 = getMockUid();
await createRecoveryCode(db, mockUid1);
await createRecoveryCode(db, mockUid1);
await createRecoveryCode(db, mockUid2);

const result1 = await backupCodeManager.getCountForUserId(mockUid1);
const result2 = await backupCodeManager.getCountForUserId(mockUid2);

expect(result1.hasBackupCodes).toBe(true);
expect(result1.count).toBe(2);
expect(result2.hasBackupCodes).toBe(true);
expect(result2.count).toBe(1);
});

it('should handle database errors gracefully', async () => {
jest.spyOn(db, 'selectFrom').mockImplementation(() => {
throw new Error('Database error');
});

await expect(
backupCodeManager.getCountForUserId(getMockUid())
).rejects.toThrow('Database error');
});
});
37 changes: 37 additions & 0 deletions libs/accounts/two-factor/src/lib/backup-code.manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import {
AccountDatabase,
AccountDbProvider,
} from '@fxa/shared/db/mysql/account';
import { Inject, Injectable } from '@nestjs/common';
import { getRecoveryCodes } from './backup-code.repository';

@Injectable()
export class BackupCodeManager {
constructor(
@Inject(AccountDbProvider) private readonly db: AccountDatabase
) {}

/**
* Gets the count of recover codes for a given uid.
*
* @param uid - The uid in hexadecimal string format.
* @returns An object containing whether the user has backup codes and the count of backup codes.
*/
async getCountForUserId(
uid: string
): Promise<{ hasBackupCodes: boolean; count: number }> {
const recoveryCodes = await getRecoveryCodes(
this.db,
Buffer.from(uid, 'hex')
);

return {
hasBackupCodes: recoveryCodes.length > 0,
count: recoveryCodes.length,
};
}
}
12 changes: 12 additions & 0 deletions libs/accounts/two-factor/src/lib/backup-code.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { AccountDatabase } from '@fxa/shared/db/mysql/account';

export async function getRecoveryCodes(db: AccountDatabase, uid: Buffer) {
return await db
.selectFrom('recoveryCodes')
.where('uid', '=', uid)
.selectAll()
.execute();
}
16 changes: 16 additions & 0 deletions libs/accounts/two-factor/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
11 changes: 11 additions & 0 deletions libs/accounts/two-factor/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
14 changes: 14 additions & 0 deletions libs/accounts/two-factor/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
1 change: 1 addition & 0 deletions libs/shared/db/mysql/account/src/lib/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ACCOUNT_TABLES =
| 'accountCustomers'
| 'paypalCustomers'
| 'carts'
| 'recoveryCodes'
| 'emails';

export async function testAccountDatabaseSetup(
Expand Down
7 changes: 7 additions & 0 deletions libs/shared/db/mysql/account/src/test/recoveryCodes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE `recoveryCodes` (
`id` bigint AUTO_INCREMENT PRIMARY KEY,
`uid` binary(16) DEFAULT NULL,
`codeHash` binary(32) DEFAULT NULL,
`salt` binary(32) DEFAULT NULL,
KEY `uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
4 changes: 3 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@
"@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"],
"@fxa/vendored/typesafe-node-firestore": [
"libs/vendored/typesafe-node-firestore/src/index.ts"
]
],
"accounts/recovery-phone": ["libs/accounts/recovery-phone/src/index.ts"],
"accounts/two-factor": ["libs/accounts/two-factor/src/index.ts"]
},
"typeRoots": [
"./types",
Expand Down

0 comments on commit e123ab9

Please sign in to comment.