Skip to content

Commit 468b757

Browse files
authored
Merge pull request #20239 from mozilla/FXA-13063
feat(passkeys): Add passkey list, delete, rename service functions
2 parents 915fdc3 + 0b2597b commit 468b757

9 files changed

Lines changed: 395 additions & 10 deletions

File tree

libs/accounts/errors/src/app-error.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,33 @@ export class AppError extends Error {
17911791
});
17921792
}
17931793

1794+
static passkeyInvalidName() {
1795+
return new AppError({
1796+
code: 400,
1797+
error: 'Bad Request',
1798+
errno: ERRNO.PASSKEY_INVALID_NAME,
1799+
message: 'Invalid passkey name',
1800+
});
1801+
}
1802+
1803+
static passkeyDeleteFailed() {
1804+
return new AppError({
1805+
code: 500,
1806+
error: 'Internal Server Error',
1807+
errno: ERRNO.PASSKEY_DELETE_FAILED,
1808+
message: 'Failed to delete passkey',
1809+
});
1810+
}
1811+
1812+
static passkeyRenameFailed() {
1813+
return new AppError({
1814+
code: 500,
1815+
error: 'Internal Server Error',
1816+
errno: ERRNO.PASSKEY_RENAME_FAILED,
1817+
message: 'Failed to rename passkey',
1818+
});
1819+
}
1820+
17941821
private static decorateErrorWithRequest(error: AppError, request?: Request) {
17951822
if (request) {
17961823
error.output.payload.request = {

libs/accounts/errors/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export const ERRNO = {
135135
PASSKEY_AUTHENTICATION_FAILED: 227,
136136
PASSKEY_REGISTRATION_FAILED: 228,
137137
PASSKEY_CHALLENGE_NOT_FOUND: 229,
138+
PASSKEY_INVALID_NAME: 230,
139+
PASSKEY_DELETE_FAILED: 231,
140+
PASSKEY_RENAME_FAILED: 232,
138141
// Jump to 238 to leave room for future passkey errors
139142
PASSKEY_CHALLENGE_EXPIRED: 238,
140143
INTERNAL_VALIDATION_ERROR: 998,

libs/accounts/errors/src/index.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,5 +449,53 @@ describe('AppErrors', () => {
449449
},
450450
});
451451
});
452+
453+
it('creates passkeyInvalidName', () => {
454+
const result = AppError.passkeyInvalidName();
455+
expect(result).toBeInstanceOf(AppError);
456+
expect(result).toMatchObject({
457+
errno: 230,
458+
message: 'Invalid passkey name',
459+
output: {
460+
statusCode: 400,
461+
payload: {
462+
error: 'Bad Request',
463+
errno: 230,
464+
},
465+
},
466+
});
467+
});
468+
469+
it('creates passkeyDeleteFailed', () => {
470+
const result = AppError.passkeyDeleteFailed();
471+
expect(result).toBeInstanceOf(AppError);
472+
expect(result).toMatchObject({
473+
errno: 231,
474+
message: 'Failed to delete passkey',
475+
output: {
476+
statusCode: 500,
477+
payload: {
478+
error: 'Internal Server Error',
479+
errno: 231,
480+
},
481+
},
482+
});
483+
});
484+
485+
it('creates passkeyRenameFailed', () => {
486+
const result = AppError.passkeyRenameFailed();
487+
expect(result).toBeInstanceOf(AppError);
488+
expect(result).toMatchObject({
489+
errno: 232,
490+
message: 'Failed to rename passkey',
491+
output: {
492+
statusCode: 500,
493+
payload: {
494+
error: 'Internal Server Error',
495+
errno: 232,
496+
},
497+
},
498+
});
499+
});
452500
});
453501
});

libs/accounts/passkey/src/lib/passkey.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import type {
1818
UserVerificationRequirement,
1919
} from '@simplewebauthn/server';
2020

21+
/**
22+
* Maximum length for a passkey's user-facing name.
23+
* Derived from the DB schema constraint: `name varchar(255)`.
24+
*/
25+
export const MAX_PASSKEY_NAME_LENGTH = 255;
26+
2127
/**
2228
* Configuration for passkey (WebAuthn) functionality.
2329
*

libs/accounts/passkey/src/lib/passkey.manager.in.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { AccountManager } from '@fxa/shared/account/account';
1313
import { LOGGER_PROVIDER } from '@fxa/shared/log';
1414
import { Test } from '@nestjs/testing';
1515
import { PasskeyManager } from './passkey.manager';
16-
import { PasskeyConfig } from './passkey.config';
16+
import { PasskeyConfig, MAX_PASSKEY_NAME_LENGTH } from './passkey.config';
1717
import { findPasskeyByCredentialId, insertPasskey } from './passkey.repository';
1818
import { StatsDService } from '@fxa/shared/metrics/statsd';
1919
import { AppError } from '../../../errors/src';
@@ -338,12 +338,12 @@ describe('PasskeyManager (Integration)', () => {
338338
expect(found?.name).toBe(passkey.name);
339339
});
340340

341-
it('accepts a name at exactly 255 characters', async () => {
341+
it(`accepts a name at exactly ${MAX_PASSKEY_NAME_LENGTH} characters`, async () => {
342342
const uid = await createTestAccount();
343343
const passkey = PasskeyFactory({ uid });
344344
await manager.registerPasskey(passkey);
345345

346-
const exactName = 'x'.repeat(255);
346+
const exactName = 'x'.repeat(MAX_PASSKEY_NAME_LENGTH);
347347
const renamed = await manager.renamePasskey(
348348
uid,
349349
passkey.credentialId,

libs/accounts/passkey/src/lib/passkey.manager.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import { LOGGER_PROVIDER } from '@fxa/shared/log';
1212
import { StatsDService } from '@fxa/shared/metrics/statsd';
1313
import { PasskeyManager } from './passkey.manager';
14-
import { PasskeyConfig } from './passkey.config';
14+
import { PasskeyConfig, MAX_PASSKEY_NAME_LENGTH } from './passkey.config';
1515
import * as PasskeyRepository from './passkey.repository';
1616
import { AppError } from '../../../errors/src';
1717

@@ -208,11 +208,11 @@ describe('PasskeyManager', () => {
208208
});
209209

210210
describe('renamePasskey', () => {
211-
it('returns false without calling repository when name exceeds 255 characters', async () => {
211+
it(`returns false without calling repository when name exceeds ${MAX_PASSKEY_NAME_LENGTH} characters`, async () => {
212212
const result = await manager.renamePasskey(
213213
Buffer.alloc(16, 1),
214214
Buffer.alloc(32, 2),
215-
'x'.repeat(256)
215+
'x'.repeat(MAX_PASSKEY_NAME_LENGTH + 1)
216216
);
217217

218218
expect(result).toBe(false);

libs/accounts/passkey/src/lib/passkey.manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
updatePasskeyCounterAndLastUsed,
2323
updatePasskeyName,
2424
} from './passkey.repository';
25-
import { PasskeyConfig } from './passkey.config';
25+
import { PasskeyConfig, MAX_PASSKEY_NAME_LENGTH } from './passkey.config';
2626
import { AppError } from '@fxa/accounts/errors';
2727

2828
/**
@@ -148,14 +148,14 @@ export class PasskeyManager {
148148
* Both uid and credentialId must match to prevent one user from renaming
149149
* another user's passkey.
150150
*
151-
* @returns true if the passkey was found and renamed, false if not found, wrong user, or name exceeds 255 characters
151+
* @returns true if the passkey was found and renamed, false if not found, wrong user, or name exceeds MAX_PASSKEY_NAME_LENGTH characters
152152
*/
153153
async renamePasskey(
154154
uid: Buffer,
155155
credentialId: Buffer,
156156
newName: string
157157
): Promise<boolean> {
158-
if (newName.length > 255) {
158+
if (newName.length > MAX_PASSKEY_NAME_LENGTH) {
159159
return false;
160160
}
161161
const rowsUpdated = await updatePasskeyName(

0 commit comments

Comments
 (0)