True passkey implement
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* Passkey REST API Endpoints — Spec Section 3.7, 16.3
|
||||
* Passkey REST API Server — Spec Section 3.7, 16.3
|
||||
*
|
||||
* Provides REST endpoints for:
|
||||
* - POST /api/passkey/register/begin — Start registration
|
||||
* - POST /api/passkey/register/complete — Finish registration
|
||||
* - POST /api/passkey/authenticate/begin — Start authentication
|
||||
* - POST /api/passkey/authenticate/complete — Finish authentication
|
||||
* Proxies browser FIDO2 requests to PasskeyPlatformService (→ amiPro FIDO2 server).
|
||||
*
|
||||
* Endpoints:
|
||||
* POST /api/passkey/attestation/options — Get registration challenge
|
||||
* POST /api/passkey/attestation/result — Submit registration result
|
||||
* POST /api/passkey/assertion/options — Get authentication challenge
|
||||
* POST /api/passkey/assertion/result — Submit authentication result
|
||||
* POST /api/passkey/session/validate — Validate a session
|
||||
* POST /api/passkey/session/delete — Delete/logout a session
|
||||
*/
|
||||
|
||||
import http from 'node:http';
|
||||
@@ -38,8 +42,8 @@ export class PasskeyAPIServer {
|
||||
const url = new URL(req.url ?? '/', 'http://localhost');
|
||||
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', this.origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
@@ -56,94 +60,108 @@ export class PasskeyAPIServer {
|
||||
try {
|
||||
const body = await parseJsonBody(req) as Record<string, unknown>;
|
||||
|
||||
if (url.pathname === '/api/passkey/register/begin') {
|
||||
await this.handleRegisterBegin(res, body);
|
||||
} else if (url.pathname === '/api/passkey/register/complete') {
|
||||
await this.handleRegisterComplete(res, body);
|
||||
} else if (url.pathname === '/api/passkey/authenticate/begin') {
|
||||
await this.handleAuthenticateBegin(res, body);
|
||||
} else if (url.pathname === '/api/passkey/authenticate/complete') {
|
||||
await this.handleAuthenticateComplete(res, body);
|
||||
} else {
|
||||
sendJson(res, 404, { error: 'Not Found' });
|
||||
switch (url.pathname) {
|
||||
case '/api/passkey/attestation/options':
|
||||
await this.handleAttestationOptions(res, body);
|
||||
break;
|
||||
case '/api/passkey/attestation/result':
|
||||
await this.handleAttestationResult(res, body);
|
||||
break;
|
||||
case '/api/passkey/assertion/options':
|
||||
await this.handleAssertionOptions(res, body);
|
||||
break;
|
||||
case '/api/passkey/assertion/result':
|
||||
await this.handleAssertionResult(res, body);
|
||||
break;
|
||||
case '/api/passkey/session/validate':
|
||||
await this.handleSessionValidate(res, body);
|
||||
break;
|
||||
case '/api/passkey/session/delete':
|
||||
await this.handleSessionDelete(res, body);
|
||||
break;
|
||||
default:
|
||||
sendJson(res, 404, { error: 'Not Found' });
|
||||
}
|
||||
} catch (err) {
|
||||
sendJson(res, 400, { error: (err as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRegisterBegin(
|
||||
// ─── Registration ────────────────────────────────────────────────────
|
||||
|
||||
private async handleAttestationOptions(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const userId = body.userId as string;
|
||||
const displayName = body.displayName as string || userId;
|
||||
const username = body.username as string;
|
||||
const displayName = (body.displayName as string) || username;
|
||||
|
||||
if (!userId) {
|
||||
sendJson(res, 400, { error: 'userId is required' });
|
||||
if (!username) {
|
||||
sendJson(res, 400, { error: 'username is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.platformService.startRegistration(userId, displayName);
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
const result = await this.platformService.getAttestationOptions(username, displayName);
|
||||
sendJson(res, result.success ? 200 : 400, result.success ? result.options : { error: result.error });
|
||||
}
|
||||
|
||||
private async handleRegisterComplete(
|
||||
private async handleAttestationResult(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const userId = body.userId as string;
|
||||
const challengeB64 = body.challenge as string;
|
||||
const attestationResponse = body.attestationResponse as Record<string, unknown>;
|
||||
const result = await this.platformService.submitAttestationResult(body as any);
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
}
|
||||
|
||||
if (!userId || !challengeB64 || !attestationResponse) {
|
||||
sendJson(res, 400, { error: 'Missing required fields' });
|
||||
// ─── Authentication ──────────────────────────────────────────────────
|
||||
|
||||
private async handleAssertionOptions(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const username = body.username as string | undefined;
|
||||
|
||||
const result = await this.platformService.getAssertionOptions(username);
|
||||
sendJson(res, result.success ? 200 : 400, result.success ? result.options : { error: result.error });
|
||||
}
|
||||
|
||||
private async handleAssertionResult(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const result = await this.platformService.submitAssertionResult(body as any);
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
}
|
||||
|
||||
// ─── Session ─────────────────────────────────────────────────────────
|
||||
|
||||
private async handleSessionValidate(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const session = body.session as string;
|
||||
if (!session) {
|
||||
sendJson(res, 400, { error: 'session is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.platformService.completeRegistration(
|
||||
userId,
|
||||
challengeB64,
|
||||
attestationResponse as any,
|
||||
this.rpId,
|
||||
this.origin,
|
||||
);
|
||||
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
const valid = await this.platformService.validateSession(session);
|
||||
sendJson(res, 200, { valid });
|
||||
}
|
||||
|
||||
private async handleAuthenticateBegin(
|
||||
private async handleSessionDelete(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const userId = body.userId as string | undefined;
|
||||
|
||||
const result = await this.platformService.startAuthentication(userId);
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
}
|
||||
|
||||
private async handleAuthenticateComplete(
|
||||
res: http.ServerResponse,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const credentialId = body.credentialId as string;
|
||||
const challengeB64 = body.challenge as string;
|
||||
const assertionResponse = body.assertionResponse as Record<string, unknown>;
|
||||
|
||||
if (!credentialId || !challengeB64 || !assertionResponse) {
|
||||
sendJson(res, 400, { error: 'Missing required fields' });
|
||||
const session = body.session as string;
|
||||
const username = body.username as string;
|
||||
if (!session || !username) {
|
||||
sendJson(res, 400, { error: 'session and username are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.platformService.completeAuthentication(
|
||||
credentialId,
|
||||
challengeB64,
|
||||
assertionResponse as any,
|
||||
this.rpId,
|
||||
this.origin,
|
||||
);
|
||||
|
||||
sendJson(res, result.success ? 200 : 400, result);
|
||||
await this.platformService.deleteSession(session, username);
|
||||
sendJson(res, 200, { ok: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user