feat(2fa): Add `backup-code` libs

This commit is contained in:
Vijay Budhram 2024-10-30 12:25:54 -04:00
Родитель 3e8bbdc416
Коммит 320a84f436
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 9778545895B2532B
17 изменённых файлов: 339 добавлений и 1 удалений

Просмотреть файл

@ -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": {}
}
]
}

Просмотреть файл

@ -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).

Просмотреть файл

@ -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',
};

Просмотреть файл

@ -0,0 +1,4 @@
{
"name": "@fxa/accounts/two-factor",
"version": "0.0.1"
}

Просмотреть файл

@ -0,0 +1,46 @@
{
"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/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"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"],
"platform": "node"
},
"configurations": {
"development": {
"minify": false
},
"production": {
"minify": true
}
}
},
"test-unit": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/accounts/two-factor/jest.config.ts",
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
}
},
"test-integration": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/accounts/two-factor/jest.config.ts",
"testPathPattern": ["\\.in\\.spec\\.ts$"]
}
}
}
}

Просмотреть файл

@ -0,0 +1 @@
export * from './lib/backup-code.manager';

Просмотреть файл

@ -0,0 +1,36 @@
/* 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';
import { RecoveryCode } from './backup-code.types';
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,
});

Просмотреть файл

@ -0,0 +1,95 @@
import { BackupCodeManager } from './backup-code.manager';
import {
AccountDatabase,
AccountDbProvider,
testAccountDatabaseSetup,
} from '@fxa/shared/db/mysql/account';
import { RecoveryCodeFactory } from './backup-code.factories';
import { faker } from '@faker-js/faker';
import { Test } from '@nestjs/testing';
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']);
const moduleRef = await Test.createTestingModule({
providers: [
BackupCodeManager,
{
provide: AccountDbProvider,
useValue: db,
},
],
}).compile();
backupCodeManager = moduleRef.get(BackupCodeManager);
});
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');
});
});

Просмотреть файл

@ -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,
};
}
}

Просмотреть файл

@ -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();
}

Просмотреть файл

@ -0,0 +1,9 @@
/* 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/. */
export interface RecoveryCode {
uid: Buffer;
codeHash: Buffer;
salt: Buffer;
}

Просмотреть файл

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

Просмотреть файл

@ -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"
]
}

Просмотреть файл

@ -15,6 +15,7 @@ export type ACCOUNT_TABLES =
| 'accountCustomers' | 'accountCustomers'
| 'paypalCustomers' | 'paypalCustomers'
| 'carts' | 'carts'
| 'recoveryCodes'
| 'emails'; | 'emails';
export async function testAccountDatabaseSetup( export async function testAccountDatabaseSetup(

Просмотреть файл

@ -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;

Просмотреть файл

@ -27,6 +27,9 @@
"@fxa/accounts/recovery-phone": [ "@fxa/accounts/recovery-phone": [
"libs/accounts/recovery-phone/src/index.ts" "libs/accounts/recovery-phone/src/index.ts"
], ],
"@fxa/accounts/two-factor": [
"libs/accounts/two-factor/src/index.ts"
],
"@fxa/payments/capability": ["libs/payments/capability/src/index.ts"], "@fxa/payments/capability": ["libs/payments/capability/src/index.ts"],
"@fxa/payments/cart": ["libs/payments/cart/src/index.ts"], "@fxa/payments/cart": ["libs/payments/cart/src/index.ts"],
"@fxa/payments/currency": ["libs/payments/currency/src/index.ts"], "@fxa/payments/currency": ["libs/payments/currency/src/index.ts"],
@ -93,7 +96,9 @@
"@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"], "@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"],
"@fxa/vendored/typesafe-node-firestore": [ "@fxa/vendored/typesafe-node-firestore": [
"libs/vendored/typesafe-node-firestore/src/index.ts" "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": [ "typeRoots": [
"./types", "./types",