diff options
Diffstat (limited to 'app/routes/auth.login.tsx')
| -rw-r--r-- | app/routes/auth.login.tsx | 124 |
1 files changed, 50 insertions, 74 deletions
diff --git a/app/routes/auth.login.tsx b/app/routes/auth.login.tsx index f8e9028..c02045b 100644 --- a/app/routes/auth.login.tsx +++ b/app/routes/auth.login.tsx @@ -1,15 +1,9 @@ import { useState } from "react"; import { redirect } from "react-router"; -import { - generateAuthenticationOptions, - verifyAuthenticationResponse, - type AuthenticationResponseJSON, - type AuthenticatorTransportFuture, -} from "@simplewebauthn/server"; +import type { AuthenticationResponseJSON, 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"; +import { getSession } from "~/lib/session.server"; +import { listCredentials } from "~/lib/db.server"; export async function loader({ request }: Route.LoaderArgs) { const session = await getSession(request.headers.get("Cookie")); @@ -18,65 +12,11 @@ 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 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 }); +function bufferToB64url(buf: ArrayBuffer): string { + const bytes = new Uint8Array(buf); + let binary = ""; + for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } export default function Login() { @@ -87,15 +27,51 @@ export default function Login() { setStatus("loading"); setErrorMsg(""); try { - const { startAuthentication } = await import("@simplewebauthn/browser"); - - const optRes = await fetch("/auth/login?intent=options", { method: "POST" }); + const optRes = await fetch("/api/passkey?intent=login-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", { + const b64urlToBuffer = (b64url: string): ArrayBuffer => { + const base64 = b64url.replace(/-/g, "+").replace(/_/g, "/"); + const pad = "=".repeat((4 - (base64.length % 4)) % 4); + const binary = atob(base64 + pad); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; + }; + + const credential = await navigator.credentials.get({ + publicKey: { + challenge: b64urlToBuffer(options.challenge as string), + rpId: options.rpId as string, + allowCredentials: (options.allowCredentials as Array<{ id: string; transports?: string[] }> ?? []).map((c) => ({ + id: b64urlToBuffer(c.id), + type: "public-key" as const, + transports: c.transports as AuthenticatorTransport[] | undefined, + })), + userVerification: "required" as UserVerificationRequirement, + timeout: 60000, + }, + }) as PublicKeyCredential | null; + + if (!credential) throw new Error("認証に失敗しました"); + + const assertion = credential.response as AuthenticatorAssertionResponse; + const authResponse: AuthenticationResponseJSON = { + id: credential.id, + rawId: bufferToB64url(credential.rawId), + response: { + clientDataJSON: bufferToB64url(assertion.clientDataJSON), + authenticatorData: bufferToB64url(assertion.authenticatorData), + signature: bufferToB64url(assertion.signature), + userHandle: assertion.userHandle ? bufferToB64url(assertion.userHandle) : undefined, + }, + authenticatorAttachment: credential.authenticatorAttachment ?? undefined, + clientExtensionResults: credential.getClientExtensionResults(), + type: "public-key", + }; + + const verRes = await fetch("/api/passkey?intent=login-verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authResponse), |
