import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, type RegistrationResponseJSON, type AuthenticationResponseJSON, type AuthenticatorTransportFuture, } from "@simplewebauthn/server"; import type { Route } from "./+types/api.passkey"; import { getSession, commitSession } from "~/lib/session.server"; import { saveCredential, listCredentials, getCredentialById, updateCredentialCounter, } from "~/lib/db.server"; import { rpID, rpName, origin } from "~/lib/passkey.server"; export async function action({ request }: Route.ActionArgs) { const url = new URL(request.url); const intent = url.searchParams.get("intent"); const session = await getSession(request.headers.get("Cookie")); if (intent === "reg-options") { const body = (await request.json()) as { token?: string }; const registerToken = process.env.REGISTER_TOKEN; if (!registerToken || body.token !== registerToken) { return Response.json({ error: "トークンが違います" }, { status: 403 }); } const options = await generateRegistrationOptions({ rpName, rpID, userID: new Uint8Array([109, 105, 99, 114, 111, 98, 108, 111, 103]), userName: "admin", userDisplayName: "admin", attestationType: "none", authenticatorSelection: { residentKey: "required", userVerification: "required" }, }); const minimalOptions = { rp: options.rp, user: options.user, challenge: options.challenge, pubKeyCredParams: options.pubKeyCredParams.filter((p) => p.alg === -7 || p.alg === -257), }; session.set("challenge", options.challenge); return Response.json(minimalOptions, { headers: { "Set-Cookie": await commitSession(session) }, }); } if (intent === "reg-verify") { const challenge = session.get("challenge") as string | undefined; if (!challenge) return Response.json({ error: "セッション切れです" }, { status: 400 }); const body = (await request.json()) as RegistrationResponseJSON; let verification; try { verification = await verifyRegistrationResponse({ response: body, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpID, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error("[passkey] verifyRegistrationResponse failed:", msg); return Response.json({ error: msg }, { status: 400 }); } if (!verification.verified || !verification.registrationInfo) { return Response.json({ error: "登録に失敗しました" }, { status: 400 }); } const { credential } = verification.registrationInfo; saveCredential({ id: credential.id, publicKey: credential.publicKey, counter: credential.counter, transports: (body.response as { transports?: AuthenticatorTransportFuture[] }).transports, }); session.unset("challenge"); session.set("authenticated", true); return Response.json({ ok: true }, { headers: { "Set-Cookie": await commitSession(session) }, }); } if (intent === "login-options") { const credentials = listCredentials(); const options = await generateAuthenticationOptions({ rpID, allowCredentials: credentials.map((c) => ({ id: c.id, transports: c.transports ? (JSON.parse(c.transports) as AuthenticatorTransportFuture[]) : undefined, })), userVerification: "required", }); session.set("challenge", options.challenge); return Response.json(options, { headers: { "Set-Cookie": await commitSession(session) }, }); } if (intent === "login-verify") { const challenge = session.get("challenge") as string | undefined; if (!challenge) return Response.json({ error: "セッション切れです" }, { status: 400 }); const body = (await request.json()) as AuthenticationResponseJSON; const credential = getCredentialById(body.id); if (!credential) return Response.json({ error: "パスキーが見つかりません" }, { status: 400 }); const verification = await verifyAuthenticationResponse({ response: body, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpID, credential: { id: credential.id, publicKey: new Uint8Array(Buffer.from(credential.public_key, "base64url")), counter: credential.counter, transports: credential.transports ? (JSON.parse(credential.transports) as AuthenticatorTransportFuture[]) : undefined, }, }); if (!verification.verified) { return Response.json({ error: "認証に失敗しました" }, { status: 400 }); } updateCredentialCounter(credential.id, verification.authenticationInfo.newCounter); session.unset("challenge"); session.set("authenticated", true); return Response.json({ ok: true }, { headers: { "Set-Cookie": await commitSession(session) }, }); } return Response.json({ error: "不正なリクエスト" }, { status: 400 }); }