Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions libs/accounts/errors/src/app-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,15 @@ export class AppError extends Error {
});
}

static passkeyChallengeNotFound() {
return new AppError({
code: 404,
error: 'Not Found',
errno: ERRNO.PASSKEY_CHALLENGE_NOT_FOUND,
message: 'Passkey challenge not found',
});
}

static passkeyAlreadyRegistered() {
return new AppError({
code: 409,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ afterAll(async () => {
describe('PasskeyChallengeManager (integration)', () => {
describe('generateRegistrationChallenge', () => {
it('stores challenge with uid and type=registration', async () => {
const challenge = await manager.generateRegistrationChallenge({
uid: 'deadbeef',
});
const challenge =
await manager.generateRegistrationChallenge('deadbeef');

const stored = await manager.validateChallenge(challenge, 'registration');
const stored = await manager.consumeRegistrationChallenge(
'deadbeef',
challenge
);

expect(stored?.challenge).toBe(challenge);
expect(stored?.type).toBe('registration');
Expand All @@ -85,65 +87,55 @@ describe('PasskeyChallengeManager (integration)', () => {
});

describe('generateAuthenticationChallenge', () => {
it('stores challenge with no uid and type=authentication', async () => {
const challenge = await manager.generateAuthenticationChallenge();
it('stores challenge with uid and type=authentication', async () => {
const challenge =
await manager.generateAuthenticationChallenge('deadbeef');

const stored = await manager.validateChallenge(
challenge,
'authentication'
const stored = await manager.consumeAuthenticationChallenge(
'deadbeef',
challenge
);

expect(stored?.challenge).toBe(challenge);
expect(stored?.type).toBe('authentication');
expect(stored?.uid).toBeUndefined();
expect(stored?.uid).toBe('deadbeef');
});
});

describe('generateUpgradeChallenge', () => {
it('stores challenge with uid and type=upgrade', async () => {
const challenge = await manager.generateUpgradeChallenge({
uid: 'cafebabe',
});
const challenge =
await manager.generateUpgradeChallenge('cafebabe');

const stored = await manager.validateChallenge(challenge, 'upgrade');
const stored = await manager.consumeUpgradeChallenge(
'cafebabe',
challenge
);

expect(stored?.challenge).toBe(challenge);
expect(stored?.type).toBe('upgrade');
expect(stored?.uid).toBe('cafebabe');
});
});

describe('validateChallenge', () => {
it('returns the stored challenge data', async () => {
const challenge = await manager.generateRegistrationChallenge({
uid: 'deadbeef',
});

const stored = await manager.validateChallenge(challenge, 'registration');

expect(stored?.challenge).toBe(challenge);
expect(stored?.type).toBe('registration');
expect(stored?.uid).toBe('deadbeef');
});

it('returns null on second validate (single-use enforcement)', async () => {
const challenge = await manager.generateRegistrationChallenge({
uid: 'deadbeef',
});
describe('consumeChallenge', () => {
it('returns null on second consume (single-use enforcement)', async () => {
const challenge =
await manager.generateRegistrationChallenge('deadbeef');

await manager.validateChallenge(challenge, 'registration');
await manager.consumeRegistrationChallenge('deadbeef', challenge);

const secondValidation = await manager.validateChallenge(
challenge,
'registration'
const secondAttempt = await manager.consumeRegistrationChallenge(
'deadbeef',
challenge
);
expect(secondValidation).toBeNull();
expect(secondAttempt).toBeNull();
});

it('returns null for an unknown challenge', async () => {
const result = await manager.validateChallenge(
'nonexistent-challenge',
'registration'
const result = await manager.consumeRegistrationChallenge(
'deadbeef',
'nonexistent-challenge'
);
expect(result).toBeNull();
});
Expand All @@ -159,38 +151,40 @@ describe('PasskeyChallengeManager (integration)', () => {
const moduleRef = await buildTestModule(redis, shortConfig, mockLogger);

const shortManager = moduleRef.get(PasskeyChallengeManager);
const challenge = await shortManager.generateRegistrationChallenge({
uid: 'deadbeef',
});
const challenge =
await shortManager.generateRegistrationChallenge('deadbeef');

// Wait longer than the TTL to ensure the challenge expires
await new Promise((resolve) => setTimeout(resolve, 1100));

const result = await shortManager.validateChallenge(
challenge,
'registration'
const result = await shortManager.consumeRegistrationChallenge(
'deadbeef',
challenge
);
expect(result).toBeNull();
}, 5_000);
});

describe('deleteChallenge', () => {
it('removes the key from Redis', async () => {
const challenge = await manager.generateRegistrationChallenge({
uid: 'deadbeef',
});
const challenge =
await manager.generateRegistrationChallenge('deadbeef');

await manager.deleteChallenge(challenge, 'registration');
await manager.deleteChallenge(challenge, 'registration', 'deadbeef');

const result = await manager.validateChallenge(challenge, 'registration');
const result = await manager.consumeRegistrationChallenge(
'deadbeef',
challenge
);

expect(result).toBeNull();
});

it('does not throw when the key does not exist', async () => {
const result = await manager.deleteChallenge(
'nonexistent-challenge',
'registration'
'registration',
'deadbeef'
);
expect(result).toBeUndefined();
});
Expand Down
Loading
Loading