diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-19 23:26:55 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-19 23:26:55 +0900 |
| commit | 85e06567f3054b6fc7349aa1f65809db47c9034a (patch) | |
| tree | c2f08baac7f46155147402ea18ce543059c76581 /app/routes/auth.register.tsx | |
| parent | cba06f7e23417a9aef6c15ae26715385feb225ba (diff) | |
Require REGISTER_TOKEN to create a passkey
Prevents anyone who finds /auth/register from registering.
Token is set via REGISTER_TOKEN env var on the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes/auth.register.tsx')
| -rw-r--r-- | app/routes/auth.register.tsx | 68 |
1 files changed, 42 insertions, 26 deletions
diff --git a/app/routes/auth.register.tsx b/app/routes/auth.register.tsx index cc4e6a9..c064eff 100644 --- a/app/routes/auth.register.tsx +++ b/app/routes/auth.register.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Link, redirect } from "react-router"; +import { redirect } from "react-router"; import { generateRegistrationOptions, verifyRegistrationResponse, @@ -8,14 +8,13 @@ import { } from "@simplewebauthn/server"; import type { Route } from "./+types/auth.register"; import { getSession, commitSession } from "~/lib/session.server"; -import { listCredentials, saveCredential } from "~/lib/db.server"; +import { saveCredential } from "~/lib/db.server"; import { rpID, rpName, 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("/"); - const hasCredentials = listCredentials().length > 0; - return { hasCredentials }; + return {}; } export async function action({ request }: Route.ActionArgs) { @@ -24,6 +23,12 @@ export async function action({ request }: Route.ActionArgs) { 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, @@ -74,8 +79,8 @@ export async function action({ request }: Route.ActionArgs) { return Response.json({ error: "不正なリクエスト" }, { status: 400 }); } -export default function Register({ loaderData }: Route.ComponentProps) { - const { hasCredentials } = loaderData; +export default function Register() { + const [token, setToken] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "error">("idle"); const [errorMsg, setErrorMsg] = useState(""); @@ -85,8 +90,15 @@ export default function Register({ loaderData }: Route.ComponentProps) { try { const { startRegistration } = await import("@simplewebauthn/browser"); - const optRes = await fetch("/auth/register?intent=options", { method: "POST" }); - if (!optRes.ok) throw new Error("オプション取得に失敗しました"); + const optRes = await fetch("/auth/register?intent=options", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token }), + }); + if (!optRes.ok) { + const err = await optRes.json(); + throw new Error(err.error ?? "オプション取得に失敗しました"); + } const options = await optRes.json(); const regResponse = await startRegistration({ optionsJSON: options }); @@ -115,24 +127,28 @@ export default function Register({ loaderData }: Route.ComponentProps) { </header> <div className="auth-box"> <h2>パスキーを登録</h2> - {hasCredentials ? ( - <> - <p>すでにパスキーが登録されています。</p> - <Link to="/auth/login" className="btn" style={{ textDecoration: "none" }}> - ログインへ - </Link> - </> - ) : ( - <> - <p> - このデバイスの生体認証(Touch ID / Face ID など)でパスキーを作成します。 - 一度登録すると、次回からはパスキーでログインできます。 - </p> - <button className="btn" onClick={handleRegister} disabled={status === "loading"}> - {status === "loading" ? "登録中…" : "パスキーを登録"} - </button> - {status === "error" && <p className="error-msg" style={{ marginTop: ".75rem" }}>{errorMsg}</p>} - </> + <p>登録トークンを入力してパスキーを作成します。</p> + <input + type="password" + placeholder="登録トークン" + value={token} + onChange={(e) => setToken(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + style={{ + width: "100%", + marginBottom: ".75rem", + padding: ".5rem .75rem", + border: "1px solid #d1d5db", + borderRadius: "4px", + fontSize: ".875rem", + fontFamily: "ui-monospace, monospace", + }} + /> + <button className="btn" onClick={handleRegister} disabled={status === "loading" || !token}> + {status === "loading" ? "登録中…" : "パスキーを登録"} + </button> + {status === "error" && ( + <p className="error-msg" style={{ marginTop: ".75rem" }}>{errorMsg}</p> )} </div> </div> |
