Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';import { PasskeyService } from './passkey.js';import { DatabaseService } from './database.js';
// Mock @simplewebauthn/servervi.mock('@simplewebauthn/server', () => ({ generateRegistrationOptions: vi.fn().mockResolvedValue({ challenge: 'mock-challenge-registration', rp: { name: 'Test', id: 'localhost' }, user: { id: 'dGVzdA', name: 'test', displayName: 'test' }, pubKeyCredParams: [{ type: 'public-key', alg: -7 }], authenticatorSelection: { residentKey: 'required', userVerification: 'preferred' }, }), verifyRegistrationResponse: vi.fn().mockResolvedValue({ verified: true, registrationInfo: { credentialID: 'cred-id-123', credentialPublicKey: Buffer.from('public-key-bytes'), counter: 0, credentialDeviceType: 'singleDevice', credentialBackedUp: false, }, }), generateAuthenticationOptions: vi.fn().mockResolvedValue({ challenge: 'mock-challenge-authentication', rpId: 'localhost', allowCredentials: [], userVerification: 'preferred', }), verifyAuthenticationResponse: vi.fn().mockResolvedValue({ verified: true, authenticationInfo: { newCounter: 1, credentialID: 'cred-id-123', }, }),}));
const PASSKEY_CONFIG = { rpName: 'Test RP', rpID: 'localhost', origin: 'http://localhost:3000',};
describe('PasskeyService', () => { let db: DatabaseService; let service: PasskeyService;
beforeEach(() => { db = new DatabaseService(':memory:'); service = new PasskeyService(db, PASSKEY_CONFIG); });
afterEach(() => { db.close(); vi.clearAllMocks(); });
describe('generateRegistrationOptions', () => { it('should return registration options', async () => { const options = await service.generateRegistrationOptions('did:plc:test', 'test.bsky.social'); expect(options).toBeDefined(); expect(options.challenge).toBe('mock-challenge-registration'); });
it('should exclude existing credentials', async () => { // Save an existing credential db.savePasskeyCredential({ id: 'existing-cred', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
await service.generateRegistrationOptions('did:plc:test', 'test.bsky.social');
const { generateRegistrationOptions } = await import('@simplewebauthn/server'); expect(generateRegistrationOptions).toHaveBeenCalledWith( expect.objectContaining({ excludeCredentials: expect.arrayContaining([ expect.objectContaining({ id: 'existing-cred' }), ]), }) ); }); });
describe('verifyRegistration', () => { it('should verify and store a credential', async () => { // First generate options to store challenge await service.generateRegistrationOptions('did:plc:test', 'test.bsky.social');
const result = await service.verifyRegistration( 'did:plc:test', 'test.bsky.social', { id: 'cred-1', rawId: 'raw', response: { clientDataJSON: 'x', attestationObject: 'y', transports: ['internal'] }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' }, 'My Passkey', );
expect(result.success).toBe(true); expect(result.credentialId).toBe('cred-id-123');
// Credential should be stored in DB const stored = db.getPasskeyCredential('cred-id-123'); expect(stored).not.toBeNull(); expect(stored!.did).toBe('did:plc:test'); });
it('should return error when no challenge exists', async () => { const result = await service.verifyRegistration( 'did:plc:unknown', 'unknown', { id: 'x', rawId: 'x', response: { clientDataJSON: 'x', attestationObject: 'y' }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any, );
expect(result.success).toBe(false); expect(result.error).toContain('No registration challenge'); });
it('should return error when challenge is expired', async () => { vi.useFakeTimers(); const now = Date.now(); vi.setSystemTime(now);
await service.generateRegistrationOptions('did:plc:test', 'test.bsky.social');
// Advance past 5 minute expiry vi.setSystemTime(now + 6 * 60 * 1000);
const result = await service.verifyRegistration( 'did:plc:test', 'test.bsky.social', { id: 'x', rawId: 'x', response: { clientDataJSON: 'x', attestationObject: 'y' }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' } as any, );
expect(result.success).toBe(false); expect(result.error).toContain('expired');
vi.useRealTimers(); }); });
describe('generateAuthenticationOptions', () => { it('should generate options without DID (discoverable)', async () => { const options = await service.generateAuthenticationOptions(); expect(options).toBeDefined(); expect(options.challenge).toBe('mock-challenge-authentication'); });
it('should include credentials when DID provided', async () => { db.savePasskeyCredential({ id: 'user-cred', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: ['internal'], name: null, });
await service.generateAuthenticationOptions('did:plc:test');
const { generateAuthenticationOptions } = await import('@simplewebauthn/server'); expect(generateAuthenticationOptions).toHaveBeenCalledWith( expect.objectContaining({ allowCredentials: expect.arrayContaining([ expect.objectContaining({ id: 'user-cred' }), ]), }) ); }); });
describe('verifyAuthentication', () => { it('should verify and return user info', async () => { // Store credential db.savePasskeyCredential({ id: 'cred-id-123', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
// Generate options to store challenge await service.generateAuthenticationOptions();
const result = await service.verifyAuthentication( { id: 'cred-id-123', rawId: 'raw', response: { clientDataJSON: 'x', authenticatorData: 'y', signature: 'z' }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' }, 'mock-challenge-authentication', );
expect(result.success).toBe(true); expect(result.did).toBe('did:plc:test'); expect(result.handle).toBe('test.bsky.social'); });
it('should return error for unknown credential', async () => { await service.generateAuthenticationOptions();
const result = await service.verifyAuthentication( { id: 'unknown-cred', rawId: 'raw', response: { clientDataJSON: 'x', authenticatorData: 'y', signature: 'z' }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' }, 'mock-challenge-authentication', );
expect(result.success).toBe(false); expect(result.error).toContain('Unknown credential'); });
it('should return error when no challenge exists', async () => { const result = await service.verifyAuthentication( { id: 'x', rawId: 'raw', response: { clientDataJSON: 'x', authenticatorData: 'y', signature: 'z' }, type: 'public-key', clientExtensionResults: {}, authenticatorAttachment: 'platform' }, 'nonexistent-challenge', );
expect(result.success).toBe(false); expect(result.error).toContain('No authentication challenge'); }); });
describe('listPasskeys', () => { it('should list passkeys for a user', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key1').toString('base64'), counter: 0, device_type: 'platform', backed_up: true, transports: ['internal'], name: 'My Macbook', });
db.savePasskeyCredential({ id: 'cred-2', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key2').toString('base64'), counter: 0, device_type: 'cross-platform', backed_up: false, transports: ['usb'], name: 'YubiKey', });
const passkeys = service.listPasskeys('did:plc:test'); expect(passkeys).toHaveLength(2); expect(passkeys[0].name).toBe('My Macbook'); expect(passkeys[1].name).toBe('YubiKey'); });
it('should return empty array for user with no passkeys', () => { const passkeys = service.listPasskeys('did:plc:nobody'); expect(passkeys).toEqual([]); }); });
describe('renamePasskey', () => { it('should rename an existing passkey', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: 'Old Name', });
const result = service.renamePasskey('did:plc:test', 'cred-1', 'New Name'); expect(result).toBe(true); });
it('should return false for wrong DID', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:other', handle: 'other.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
const result = service.renamePasskey('did:plc:test', 'cred-1', 'New Name'); expect(result).toBe(false); });
it('should return false for nonexistent credential', () => { const result = service.renamePasskey('did:plc:test', 'nonexistent', 'Name'); expect(result).toBe(false); }); });
describe('deletePasskey', () => { it('should delete an existing passkey', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:test', handle: 'test.bsky.social', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
const result = service.deletePasskey('did:plc:test', 'cred-1'); expect(result).toBe(true); expect(db.getPasskeyCredential('cred-1')).toBeNull(); });
it('should return false for wrong DID', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:other', handle: 'other', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
const result = service.deletePasskey('did:plc:test', 'cred-1'); expect(result).toBe(false); }); });
describe('hasPasskeys / getPasskeyCount', () => { it('should return false and 0 for user with no passkeys', () => { expect(service.hasPasskeys('did:plc:nobody')).toBe(false); expect(service.getPasskeyCount('did:plc:nobody')).toBe(0); });
it('should return true and correct count', () => { db.savePasskeyCredential({ id: 'cred-1', did: 'did:plc:test', handle: 'test', public_key: Buffer.from('key').toString('base64'), counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null, });
expect(service.hasPasskeys('did:plc:test')).toBe(true); expect(service.getPasskeyCount('did:plc:test')).toBe(1); }); });});