Implement WebAuthn passkey authentication
Add complete WebAuthn support for passkey registration and authentication: - Install @simplewebauthn/server for WebAuthn utilities - Create passkey-helpers.ts with base64url/Uint8Array conversion utilities - Create webauthn.ts with registration/authentication option generation and verification - Create context.ts with API context types - Implement all WebAuthn router handlers (createRegistrationOptions, verifyRegistration, createAuthenticationOptions, verifyAuthentication) - Implement passkey management handlers (listPasskeys, createPasskey, renamePasskey, deletePasskey) - Add WebAuthn configuration constants and environment variables Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
93
apps/api-server/src/utils/passkey-helpers.ts
Normal file
93
apps/api-server/src/utils/passkey-helpers.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Passkey data helpers for converting between database and WebAuthn formats
|
||||
*/
|
||||
|
||||
import type { AuthenticatorTransportFuture } from "@simplewebauthn/types";
|
||||
|
||||
/**
|
||||
* Convert a base64url string to a Uint8Array for BYTEA storage
|
||||
*/
|
||||
export const base64urlToUint8Array = (base64url: string): Uint8Array => {
|
||||
return Uint8Array.from(Buffer.from(base64url, "base64url"));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Uint8Array (from BYTEA) to a base64url string
|
||||
*/
|
||||
export const uint8ArrayToBase64url = (uint8Array: Uint8Array): string => {
|
||||
return Buffer.from(uint8Array).toString("base64url");
|
||||
};
|
||||
|
||||
/**
|
||||
* Parsed passkey data for use with simplewebauthn
|
||||
*/
|
||||
export interface ParsedPasskey {
|
||||
id: number;
|
||||
credentialId: string;
|
||||
publicKey: Uint8Array;
|
||||
counter: number;
|
||||
transports: AuthenticatorTransportFuture[] | null;
|
||||
deviceType: "singleDevice" | "multiDevice";
|
||||
backupEligible: boolean;
|
||||
backupStatus: boolean;
|
||||
rpid: string;
|
||||
name: string;
|
||||
lastUsedAt: Date | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw passkey row from database
|
||||
*/
|
||||
export interface PasskeyRow {
|
||||
id: number;
|
||||
user_id: number;
|
||||
credential_id: Uint8Array;
|
||||
public_key: Uint8Array;
|
||||
webauthn_user_id: string;
|
||||
counter: string | number | bigint;
|
||||
device_type: "singleDevice" | "multiDevice";
|
||||
backup_eligible: boolean;
|
||||
backup_status: boolean;
|
||||
transports: unknown;
|
||||
rpid: string;
|
||||
name: string;
|
||||
last_used_at: Date | null;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a passkey row from the database into a usable format
|
||||
*/
|
||||
export const parsePasskeyRow = (row: PasskeyRow): ParsedPasskey => {
|
||||
// Create a new Uint8Array to ensure proper ArrayBuffer type
|
||||
const publicKeyBytes = new Uint8Array(row.public_key);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
credentialId: uint8ArrayToBase64url(row.credential_id),
|
||||
publicKey: publicKeyBytes,
|
||||
counter: Number(row.counter),
|
||||
transports: row.transports as AuthenticatorTransportFuture[] | null,
|
||||
deviceType: row.device_type,
|
||||
backupEligible: row.backup_eligible,
|
||||
backupStatus: row.backup_status,
|
||||
rpid: row.rpid,
|
||||
name: row.name,
|
||||
lastUsedAt: row.last_used_at,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a date for passkey name
|
||||
*/
|
||||
export const formatPasskeyDate = (date: Date): string => {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user