True passkey implement

This commit is contained in:
qingjie.du
2026-03-31 15:59:59 +09:00
parent 73ce0ef5a2
commit ec5eb0a447
13 changed files with 1195 additions and 412 deletions

View File

@@ -0,0 +1,140 @@
/**
* End-to-end test for Passkey authentication against amiPro FIDO2 server.
*
* Tests:
* 1. AmiProFIDO2Client connectivity
* 2. PasskeyPlatformService flow
* 3. PasskeyServer interactive ceremony (opens browser)
*
* Usage: npx tsx test-passkey.ts [register|authenticate]
*/
import { AmiProFIDO2Client } from './src/passkey/amipro-fido2-client.js';
import { PasskeyPlatformService } from './src/passkey/platform-service.js';
import { PasskeyServer } from './src/passkey/passkey-server.js';
const FIDO2_SERVER_URL = 'https://fido2.epk.amipro.me';
const RP_ID = 'a2a-demo.mcplet.ai';
const TEST_USER = 'a2a-test-user';
async function testAmiProClient() {
console.log('\n=== Test 1: AmiProFIDO2Client Connectivity ===\n');
const client = new AmiProFIDO2Client(FIDO2_SERVER_URL, RP_ID);
// Test attestation options (registration)
console.log('[1a] Getting attestation options...');
try {
const attOpts = await client.getAttestationOptions(TEST_USER, 'A2A Test User');
console.log(' status:', attOpts.status);
if (attOpts.status !== 'failed') {
console.log(' challenge:', attOpts.challenge?.substring(0, 30) + '...');
console.log(' rp:', JSON.stringify(attOpts.rp));
console.log(' user.id:', attOpts.user?.id?.substring(0, 20) + '...');
console.log(' pubKeyCredParams:', JSON.stringify(attOpts.pubKeyCredParams));
console.log(' timeout:', attOpts.timeout);
} else {
console.log(' errorMessage:', attOpts.errorMessage);
}
} catch (err) {
console.error(' ERROR:', (err as Error).message);
}
// Test assertion options (authentication)
console.log('\n[1b] Getting assertion options...');
try {
const assertOpts = await client.getAssertionOptions(TEST_USER);
console.log(' status:', assertOpts.status);
if (assertOpts.status === 'ok') {
console.log(' challenge:', assertOpts.challenge?.substring(0, 30) + '...');
console.log(' rpId:', assertOpts.rpId);
console.log(' timeout:', assertOpts.timeout);
console.log(' allowCredentials:', assertOpts.allowCredentials?.length, 'entries');
console.log(' userVerification:', assertOpts.userVerification);
} else {
console.log(' errorMessage:', assertOpts.errorMessage);
console.log(' (Expected: user not registered yet)');
}
} catch (err) {
console.error(' ERROR:', (err as Error).message);
}
}
async function testPlatformService() {
console.log('\n=== Test 2: PasskeyPlatformService ===\n');
const service = new PasskeyPlatformService(RP_ID, FIDO2_SERVER_URL);
// Test get attestation options
console.log('[2a] Getting attestation options via service...');
const attResult = await service.getAttestationOptions(TEST_USER, 'A2A Test User');
console.log(' success:', attResult.success);
if (attResult.success) {
console.log(' challenge present:', !!attResult.options?.challenge);
console.log(' rp:', JSON.stringify(attResult.options?.rp));
} else {
console.log(' error:', attResult.error);
}
// Test get assertion options
console.log('\n[2b] Getting assertion options via service...');
const assertResult = await service.getAssertionOptions(TEST_USER);
console.log(' success:', assertResult.success);
if (assertResult.success) {
console.log(' challenge present:', !!assertResult.options?.challenge);
console.log(' allowCredentials:', assertResult.options?.allowCredentials?.length, 'entries');
} else {
console.log(' error:', assertResult.error);
console.log(' (Expected if user not registered yet)');
}
}
async function testInteractiveCeremony(username?: string) {
console.log(`\n=== Test 3: Interactive Passkey Ceremony ===\n`);
console.log(`Opens a browser page that auto-detects register vs authenticate.`);
console.log(`rpId: ${RP_ID}, FIDO2 server: ${FIDO2_SERVER_URL}`);
if (username) console.log(`Pre-filled username: ${username}`);
console.log();
const server = new PasskeyServer(RP_ID, FIDO2_SERVER_URL, 180_000);
try {
const payload = await server.startCeremony(
`この操作には Passkey 認証が必要です`,
username,
);
console.log('\n Ceremony succeeded!');
console.log(' type:', payload.type);
console.log(' challenge:', payload.challenge?.substring(0, 30) + '...');
console.log(' session:', (payload as any).session);
console.log(' username:', (payload as any).username);
} catch (err) {
console.error('\n Ceremony failed:', (err as Error).message);
}
}
// ─── Main ──────────────────────────────────────────────────────────────
const arg = process.argv[2];
await testAmiProClient();
await testPlatformService();
if (arg === 'test') {
// Interactive test: opens browser with unified flow
// Pass a username to pre-fill, or omit to let user enter it
const username = process.argv[3] || undefined;
await testInteractiveCeremony(username);
} else if (arg === 'new') {
// Test with a brand new user (no pre-fill)
await testInteractiveCeremony();
} else {
console.log('\n=== Interactive test skipped ===');
console.log('Usage:');
console.log(' npx tsx test-passkey.ts test [username] — unified flow (auto register+auth)');
console.log(' npx tsx test-passkey.ts new — new user (empty username field)');
}
console.log('\nDone.');
process.exit(0);