diff options
Diffstat (limited to 'app/routes/auth.register.tsx')
| -rw-r--r-- | app/routes/auth.register.tsx | 113 |
1 files changed, 13 insertions, 100 deletions
diff --git a/app/routes/auth.register.tsx b/app/routes/auth.register.tsx index 43e9c71..7258d38 100644 --- a/app/routes/auth.register.tsx +++ b/app/routes/auth.register.tsx @@ -1,15 +1,8 @@ import { useState } from "react"; import { redirect } from "react-router"; -import { - generateRegistrationOptions, - verifyRegistrationResponse, - type RegistrationResponseJSON, - type AuthenticatorTransportFuture, -} from "@simplewebauthn/server"; import type { Route } from "./+types/auth.register"; -import { getSession, commitSession } from "~/lib/session.server"; -import { saveCredential } from "~/lib/db.server"; -import { rpID, rpName, origin } from "~/lib/passkey.server"; +import { getSession } from "~/lib/session.server"; +import type { RegistrationResponseJSON, AuthenticatorTransportFuture } from "@simplewebauthn/server"; export async function loader({ request }: Route.LoaderArgs) { const session = await getSession(request.headers.get("Cookie")); @@ -17,86 +10,6 @@ export async function loader({ request }: Route.LoaderArgs) { 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 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]), // "microblog" - userName: "admin", - userDisplayName: "admin", - attestationType: "none", - authenticatorSelection: { - residentKey: "required", - userVerification: "required", - }, - }); - // 必須フィールドのみ送信(@simplewebauthn/browser を使わず直接 API を叩くため) - 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 === "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 redirect("/", { - headers: { "Set-Cookie": await commitSession(session) }, - }); - } - - return Response.json({ error: "不正なリクエスト" }, { status: 400 }); -} - function b64urlToBuffer(b64url: string): ArrayBuffer { const base64 = b64url.replace(/-/g, "+").replace(/_/g, "/"); const pad = "=".repeat((4 - (base64.length % 4)) % 4); @@ -126,7 +39,7 @@ export default function Register() { setStep(""); try { setStep("1a. fetch 中…"); await tick(); - const optRes = await fetch("/auth/register?intent=options", { + const optRes = await fetch("/api/passkey?intent=reg-options", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), @@ -145,21 +58,21 @@ export default function Register() { } setStep("1d. JSON パース OK"); await tick(); - const userId = b64urlToBuffer(optJSON.user.id); - const challenge = b64urlToBuffer(optJSON.challenge); + const userId = b64urlToBuffer(optJSON.user as never as string); + const userObj = optJSON.user as Record<string, string>; + const challenge = b64urlToBuffer(optJSON.challenge as string); setStep("2. credentials.create() 呼び出し中…"); await tick(); - // rp.id / authenticatorSelection / attestation を省略して最小構成で試す const credential = await navigator.credentials.create({ publicKey: { - rp: { name: optJSON.rp.name }, + rp: { name: (optJSON.rp as Record<string, string>).name }, user: { - id: userId, - name: optJSON.user.name, - displayName: optJSON.user.displayName ?? optJSON.user.name, + id: b64urlToBuffer(userObj.id), + name: userObj.name, + displayName: userObj.displayName ?? userObj.name, }, challenge, - pubKeyCredParams: optJSON.pubKeyCredParams, + pubKeyCredParams: optJSON.pubKeyCredParams as PublicKeyCredentialParameters[], }, }) as PublicKeyCredential | null; @@ -182,7 +95,7 @@ export default function Register() { type: "public-key", }; - const verRes = await fetch("/auth/register?intent=verify", { + const verRes = await fetch("/api/passkey?intent=reg-verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(regResponse), @@ -227,7 +140,7 @@ export default function Register() { {status === "loading" ? "登録中…" : "パスキーを登録"} </button> {step && ( - <p style={{ marginTop: ".75rem", fontSize: ".8rem", color: "#6b7280" }}>{step}</p> + <p style={{ marginTop: ".75rem", fontSize: ".75rem", color: "#6b7280", wordBreak: "break-all" }}>{step}</p> )} {status === "error" && ( <p className="error-msg" style={{ marginTop: ".5rem" }}>{errorMsg}</p> |
