зеркало из https://github.com/mozilla/fxa.git
feat(2fa): Add `backup-code` libs
This commit is contained in:
Родитель
3e8bbdc416
Коммит
320a84f436
|
@ -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",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче