UNPKG

expo-passkey

Version:

Passkey authentication for Expo apps with Better Auth integration

126 lines 5.07 kB
/** * @file WebAuthn challenge endpoint * @description Creates and stores challenges for WebAuthn registration and authentication */ import { createAuthEndpoint } from "better-auth/api"; import { APIError } from "better-call"; import crypto from "crypto"; import { challengeSchema } from "../utils/schema"; /** * Creates a WebAuthn challenge endpoint for registration and authentication */ export const createChallengeEndpoint = (options) => { const { logger, schemaConfig } = options; return createAuthEndpoint("/expo-passkey/challenge", { method: "POST", body: challengeSchema, metadata: { openapi: { description: "Generate a WebAuthn challenge for passkey registration or authentication", tags: ["Authentication"], responses: { 200: { description: "Challenge successfully generated", content: { "application/json": { schema: { type: "object", properties: { challenge: { type: "string" }, }, }, }, }, }, 400: { description: "Invalid request", content: { "application/json": { schema: { type: "object", properties: { error: { type: "object", properties: { code: { type: "string" }, message: { type: "string" }, }, }, }, }, }, }, }, }, }, }, }, async (ctx) => { const { userId, type, registrationOptions } = ctx.body; try { logger.debug("Generating WebAuthn challenge:", { userId, type, }); // Verify user exists (for registration) if (type === "registration") { const user = await ctx.context.adapter.findOne({ model: "user", where: [{ field: "id", operator: "eq", value: userId }], }); if (!user) { logger.warn("Challenge generation failed: User not found", { userId, }); throw new APIError("BAD_REQUEST", { code: "USER_NOT_FOUND", message: "User not found", }); } } // Generate a random challenge with sufficient entropy const randomBytes = crypto.randomBytes(32); const challenge = randomBytes.toString("base64url"); // Calculate expiration (5 minutes from now) const now = new Date(); const expiresAt = new Date(now.getTime() + 5 * 60 * 1000); // Store challenge in database await ctx.context.adapter.create({ model: schemaConfig.passkeyChallengeModel, data: { id: ctx.context.generateId({ model: schemaConfig.passkeyChallengeModel, size: 32, }), userId, challenge, type, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString(), // Store registration options if provided (for registration challenges) registrationOptions: registrationOptions ? JSON.stringify(registrationOptions) : null, }, }); logger.debug("Challenge generated successfully", { userId, type, challengeLength: challenge.length, }); // Return the challenge return ctx.json({ challenge, }); } catch (error) { logger.error("Failed to generate challenge:", error); if (error instanceof APIError) throw error; throw new APIError("INTERNAL_SERVER_ERROR", { code: "CHALLENGE_GENERATION_FAILED", message: "Failed to generate challenge", }); } }); }; //# sourceMappingURL=challenge.js.map