summaryrefslogtreecommitdiff
path: root/app/routes/auth.login.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/routes/auth.login.tsx')
-rw-r--r--app/routes/auth.login.tsx124
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),