Skip to content

Commit 77926dc

Browse files
authored
Merge pull request #19984 from mozilla/FXA-12900
feat(libs): Create scaffolding for passkeys library
2 parents d1653ee + dbd7b56 commit 77926dc

18 files changed

Lines changed: 572 additions & 2 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
}
17+
]
18+
}

libs/accounts/passkey/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# accounts-passkey
2+
3+
Passkey (WebAuthn) authentication library for Firefox Accounts.
4+
5+
This library was generated with [Nx](https://nx.dev).
6+
7+
## Building
8+
9+
Run `nx run accounts-passkey:build` to build the library.
10+
11+
## Running unit tests
12+
13+
Run `nx run accounts-passkey:test-unit` to execute the unit tests via [Jest](https://jestjs.io).
14+
15+
## Running integration tests
16+
17+
Make sure local infrastructure (databases) are running. Check status with `yarn pm2 status` - you should see redis and mysql instances running. If not, run `yarn start infrastructure`.
18+
19+
Run `nx run accounts-passkey:test-integration` to execute the integration tests via [Jest](https://jestjs.io).
20+
21+
## Architecture
22+
23+
This library follows the layered architecture pattern used across `libs/accounts/*`:
24+
25+
### Layers
26+
27+
1. **Service Layer** (`passkey.service.ts`)
28+
- High-level business logic and orchestration
29+
- Validates input, coordinates operations
30+
- Handles metrics and logging
31+
- Called by route handlers in auth-server/admin-server
32+
33+
2. **Manager Layer** (`passkey.manager.ts`)
34+
- Injectable class that wraps database operations
35+
- Coordinates repository function calls
36+
- Handles transactions and business logic related to data
37+
- Injected into Service layer
38+
39+
3. **Repository Layer** (`passkey.repository.ts`)
40+
- Pure functions that accept `AccountDatabase` as first parameter
41+
- Direct SQL queries using Kysely query builder
42+
- No business logic, just data access
43+
- Called by Manager layer
44+
45+
4. **Error Layer** (`passkey.errors.ts`)
46+
- Domain-specific error classes
47+
- Structured error information for logging
48+
- HTTP status code mapping
49+
50+
5. **Configuration** (`passkey.config.ts`)
51+
- Type-safe configuration interface
52+
- Validated with class-validator decorators
53+
- Loaded from Convict config in consuming applications
54+
55+
### Pattern: No Module Export
56+
57+
Unlike `libs/shared/nestjs/*`, this library **does not export a NestJS module**. This is intentional:
58+
59+
- **auth-server** (Hapi + TypeDI): Uses `Container.get(PasskeyService)`
60+
- **admin-server** (NestJS): Manually wires providers in their modules
61+
62+
This pattern gives consuming applications full control over DI setup.
63+
64+
## WebAuthn / Passkey Background
65+
66+
Passkeys are a WebAuthn-based authentication method that replaces passwords:
67+
68+
- **Registration (Attestation)**: Create a new passkey credential
69+
- **Authentication (Assertion)**: Verify using existing passkey
70+
- **Challenge-Response**: Server generates challenge, client signs it
71+
- **Public Key Cryptography**: Private key stays on device, public key stored in DB
72+
73+
Key WebAuthn concepts:
74+
75+
- **Relying Party (RP)**: Our service (accounts.firefox.com)
76+
- **Authenticator**: User's device (phone, laptop, security key)
77+
- **Credential ID**: Unique identifier for each passkey
78+
- **Counter**: Signature counter for replay attack prevention
79+
- **User Verification (UV)**: Biometric or PIN on the device
80+
81+
### Resources
82+
83+
- [WebAuthn Spec](https://www.w3.org/TR/webauthn-3/)
84+
- [Passkey Developer Guide](https://passkeys.dev/)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Config } from 'jest';
2+
3+
/* eslint-disable */
4+
const config: Config = {
5+
displayName: 'passkey',
6+
preset: '../../../jest.preset.js',
7+
testEnvironment: 'node',
8+
transform: {
9+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
10+
},
11+
moduleFileExtensions: ['ts', 'js', 'html'],
12+
coverageDirectory: '../../../coverage/libs/accounts/passkey',
13+
reporters: [
14+
'default',
15+
[
16+
'jest-junit',
17+
{
18+
outputDirectory: 'artifacts/tests/accounts-passkey',
19+
// It is critical that the package_name here is unique among all
20+
// Jest configs. This file is uploaded to GCS and will error on upload
21+
// if not unique because permissions for the upload deliberately prevent
22+
// overwriting files.
23+
outputName: 'accounts-passkey-jest-unit-results.xml',
24+
},
25+
],
26+
],
27+
};
28+
29+
export default config;

libs/accounts/passkey/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@fxa/accounts/passkey",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "commonjs",
6+
"main": "./index.cjs",
7+
"types": "./index.d.ts",
8+
"dependencies": {}
9+
}

libs/accounts/passkey/project.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "accounts-passkey",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/accounts/passkey/src",
5+
"projectType": "library",
6+
"tags": ["scope:shared:lib"],
7+
"targets": {
8+
"build": {
9+
"executor": "@nx/esbuild:esbuild",
10+
"outputs": ["{options.outputPath}"],
11+
"options": {
12+
"outputPath": "dist/libs/accounts/passkey",
13+
"main": "libs/accounts/passkey/src/index.ts",
14+
"tsConfig": "libs/accounts/passkey/tsconfig.lib.json",
15+
"assets": ["libs/accounts/passkey/*.md"],
16+
"format": ["cjs"],
17+
"generatePackageJson": true,
18+
"declaration": true
19+
}
20+
},
21+
"test-unit": {
22+
"executor": "@nx/jest:jest",
23+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
24+
"options": {
25+
"jestConfig": "libs/accounts/passkey/jest.config.ts",
26+
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
27+
}
28+
},
29+
"test-integration": {
30+
"executor": "@nx/jest:jest",
31+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
32+
"options": {
33+
"jestConfig": "libs/accounts/passkey/jest.config.ts",
34+
"testPathPattern": ["\\.in\\.spec\\.ts$"]
35+
}
36+
}
37+
}
38+
}

libs/accounts/passkey/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @fxa/accounts/passkey
3+
*
4+
* Passkey (WebAuthn) authentication library for Firefox Accounts.
5+
*
6+
* This library provides passkey registration and authentication functionality
7+
* following the WebAuthn specification.
8+
*
9+
* Usage:
10+
* - PasskeyService: High-level business logic for passkey operations
11+
* - PasskeyManager: Database access layer for passkey storage
12+
* - PasskeyError: Base error class for passkey-specific errors
13+
* - PasskeyConfig: Configuration class
14+
*
15+
* @packageDocumentation
16+
*/
17+
export * from './lib/passkey.service';
18+
export * from './lib/passkey.manager';
19+
export * from './lib/passkey.errors';
20+
export * from './lib/passkey.config';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { IsArray, IsBoolean, IsNumber, IsString } from 'class-validator';
6+
7+
/**
8+
* Configuration for passkey (WebAuthn) functionality.
9+
*
10+
* This configuration is loaded from Convict in auth-server's config/index.ts
11+
* and passed to PasskeyService constructor.
12+
*/
13+
export class PasskeyConfig {
14+
/**
15+
* Feature flag to enable/disable passkey functionality.
16+
*/
17+
@IsBoolean()
18+
public enabled?: boolean;
19+
20+
/**
21+
* WebAuthn Relying Party ID (must match the domain).
22+
* @example 'accounts.firefox.com'
23+
*/
24+
@IsString()
25+
public rpId!: string;
26+
27+
/**
28+
* WebAuthn Relying Party display name.
29+
* @example 'Mozilla Accounts'
30+
*/
31+
@IsString()
32+
public rpName!: string;
33+
34+
/**
35+
* Allowed origins for WebAuthn credential creation and authentication.
36+
* Must include protocol and domain.
37+
* @example ['https://accounts.firefox.com', 'https://accounts.stage.mozaws.net']
38+
*/
39+
@IsArray()
40+
public allowedOrigins!: Array<string>;
41+
42+
/**
43+
* Maximum number of passkeys a user can register.
44+
*/
45+
@IsNumber()
46+
public maxPasskeysPerUser?: number;
47+
48+
/**
49+
* Challenge expiration timeout in milliseconds.
50+
* @example 300000 (5 minutes)
51+
*/
52+
@IsNumber()
53+
public challengeTimeout?: number;
54+
55+
/**
56+
* User verification requirement for WebAuthn.
57+
* - 'required': User verification must occur (e.g., biometric, PIN)
58+
* - 'preferred': User verification preferred but not required
59+
* - 'discouraged': User verification should not occur
60+
* @example 'required'
61+
*/
62+
@IsString()
63+
public userVerification?: string;
64+
65+
/**
66+
* Resident key (discoverable credential) requirement.
67+
* - 'required': Credential must be discoverable (stored on authenticator)
68+
* - 'preferred': Discoverable credential preferred but not required
69+
* - 'discouraged': Non-discoverable credential preferred
70+
* @example 'preferred'
71+
*/
72+
@IsString()
73+
public residentKey?: string;
74+
75+
/**
76+
* Authenticator attachment preference.
77+
* - 'platform': Platform authenticators (built into device, like Touch ID)
78+
* - 'cross-platform': Roaming authenticators (USB security keys)
79+
* - undefined: No preference (allow any)
80+
*/
81+
@IsString()
82+
public authenticatorAttachment?: string;
83+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { BaseError } from '@fxa/shared/error';
6+
7+
/**
8+
* Base error class for passkey-related errors.
9+
*
10+
* Subclasses should use descriptive names ending in "Error" and include
11+
* context (e.g., PasskeyNotFoundError, not NotFoundError).
12+
*
13+
* Error numbers (errno) must be defined in the central ERRNO object at
14+
* libs/accounts/errors/src/constants.ts and imported for use in error info.
15+
*
16+
* @see libs/accounts/recovery-phone/src/lib/recovery-phone.errors.ts
17+
*/
18+
export class PasskeyError extends BaseError {
19+
/**
20+
* Creates a PasskeyError.
21+
*
22+
* @param message - Human-readable error message
23+
* @param info - Structured data for logging and debugging (examples: { errno: 1001, uid: '1234' })
24+
* @param cause - Optional underlying error that caused this error
25+
*
26+
* The resulting error object contains:
27+
* @property {string} name - Always 'PasskeyError' for base class
28+
* @property {object} info - Structured info object that provides context for the error (e.g., errno, uid)
29+
* @property {Error} [cause] - The underlying error if provided
30+
*/
31+
constructor(message: string, info: Record<string, any>, cause?: Error) {
32+
super(message, {
33+
name: 'PasskeyError',
34+
cause,
35+
info,
36+
});
37+
}
38+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { PasskeyManager } from './passkey.manager';
6+
import {
7+
AccountDatabase,
8+
AccountDbProvider,
9+
testAccountDatabaseSetup,
10+
} from '@fxa/shared/db/mysql/account';
11+
import { Test } from '@nestjs/testing';
12+
13+
describe('PasskeyManager (Integration)', () => {
14+
let manager: PasskeyManager;
15+
let db: AccountDatabase;
16+
17+
beforeAll(async () => {
18+
// Set up real database connection for integration tests
19+
// TODO: Add 'passkeys' table to the setup array once the table schema is created
20+
try {
21+
db = await testAccountDatabaseSetup(['accounts']);
22+
23+
const moduleRef = await Test.createTestingModule({
24+
providers: [
25+
PasskeyManager,
26+
{
27+
provide: AccountDbProvider,
28+
useValue: db,
29+
},
30+
],
31+
}).compile();
32+
33+
manager = moduleRef.get(PasskeyManager);
34+
} catch (error) {
35+
// Log helpful message before failing fast
36+
console.warn('\n⚠️ Integration tests require database infrastructure.');
37+
console.warn(
38+
' Run "yarn start infrastructure" to enable these tests.\n'
39+
);
40+
// Re-throw to fail fast instead of running tests that will skip
41+
throw error;
42+
}
43+
});
44+
45+
afterAll(async () => {
46+
if (db) {
47+
await db.destroy();
48+
}
49+
});
50+
51+
// TODO: Add actual integration tests once:
52+
// 1. Passkey database schema is defined
53+
// 2. PasskeyManager methods are implemented
54+
// 3. Test data factories are created
55+
it('should be defined', () => {
56+
expect(manager).toBeDefined();
57+
});
58+
});

0 commit comments

Comments
 (0)