import { useState } from "react"; import { redirect } from "react-router"; import { generateAuthenticationOptions, verifyAuthenticationResponse, type AuthenticationResponseJSON, type AuthenticatorTransportFuture, } from "@simplewebauthn/server"; import type { Route } from "./+types/auth.login"; import { getSession, commitSession } from "~/lib/session.server"; import { listCredentials, getCredentialById, updateCredentialCounter } from "~/lib/db.server"; import { rpID, origin } from "~/lib/passkey.server"; export async function loader({ request }: Route.LoaderArgs) { const session = await getSession(request.headers.get("Cookie")); if (session.get("authenticated")) return redirect("/"); if (listCredentials().length === 0) return redirect("/auth/register"); return {}; } 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 === "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 === "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 redirect("/", { headers: { "Set-Cookie": await commitSession(session) }, }); } return Response.json({ error: "不正なリクエスト" }, { status: 400 }); } export default function Login() { const [status, setStatus] = useState<"idle" | "loading" | "error">("idle"); const [errorMsg, setErrorMsg] = useState(""); async function handleLogin() { setStatus("loading"); setErrorMsg(""); try { const { startAuthentication } = await import("@simplewebauthn/browser"); const optRes = await fetch("/auth/login?intent=options", { method: "POST" }); if (!optRes.ok) throw new Error("オプション取得に失敗しました"); const options = await optRes.json(); const authResponse = await startAuthentication({ optionsJSON: options }); const verRes = await fetch("/auth/login?intent=verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authResponse), }); if (!verRes.ok) { const err = await verRes.json(); throw new Error(err.error ?? "認証に失敗しました"); } window.location.href = "/"; } catch (e) { setStatus("error"); setErrorMsg(e instanceof Error ? e.message : "エラーが発生しました"); } } return (

log

ログイン

登録済みのパスキーで認証します。

{status === "error" && (

{errorMsg}

)}
); }