diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-20 00:26:47 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-20 00:26:47 +0900 |
| commit | 6de9ab5bab41ae4c2d8e06a72362cf6bf2375598 (patch) | |
| tree | 8813799f32f4fceed968ab0b22083462e4736d92 /app/routes | |
| parent | 9da6a06bea17c26315225f394d997ff5379c8ad2 (diff) | |
Bypass @simplewebauthn/browser: call WebAuthn API directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes')
| -rw-r--r-- | app/routes/auth.register.tsx | 64 |
1 files changed, 56 insertions, 8 deletions
diff --git a/app/routes/auth.register.tsx b/app/routes/auth.register.tsx index 06f42f5..75a7d7c 100644 --- a/app/routes/auth.register.tsx +++ b/app/routes/auth.register.tsx @@ -41,8 +41,7 @@ export async function action({ request }: Route.ActionArgs) { userVerification: "required", }, }); - // iOS Safari の parseCreationOptionsFromJSON は unknown フィールドで SyntaxError を投げるため - // 必須フィールドのみ送信して原因を絞り込む + // 必須フィールドのみ送信(@simplewebauthn/browser を使わず直接 API を叩くため) const minimalOptions = { rp: options.rp, user: options.user, @@ -51,7 +50,6 @@ export async function action({ request }: Route.ActionArgs) { (p) => p.alg === -7 || p.alg === -257 ), }; - console.log("[passkey] options sent to browser:", JSON.stringify(minimalOptions)); session.set("challenge", options.challenge); return Response.json(minimalOptions, { headers: { "Set-Cookie": await commitSession(session) }, @@ -99,6 +97,22 @@ export async function action({ request }: Route.ActionArgs) { 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); + 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; +} + +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 Register() { const [token, setToken] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "error">("idle"); @@ -108,8 +122,6 @@ export default function Register() { setStatus("loading"); setErrorMsg(""); try { - const { startRegistration } = await import("@simplewebauthn/browser"); - const optRes = await fetch("/auth/register?intent=options", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -119,9 +131,45 @@ export default function Register() { const err = await optRes.json(); throw new Error(err.error ?? "オプション取得に失敗しました"); } - const options = await optRes.json(); - - const regResponse = await startRegistration({ optionsJSON: options }); + const optJSON = await optRes.json(); + + // @simplewebauthn/browser を使わず WebAuthn API を直接呼ぶ + const credential = await navigator.credentials.create({ + publicKey: { + rp: optJSON.rp, + user: { + id: b64urlToBuffer(optJSON.user.id), + name: optJSON.user.name, + displayName: optJSON.user.displayName ?? optJSON.user.name, + }, + challenge: b64urlToBuffer(optJSON.challenge), + pubKeyCredParams: optJSON.pubKeyCredParams, + timeout: 60000, + attestation: "none", + authenticatorSelection: { + residentKey: "required" as ResidentKeyRequirement, + userVerification: "required" as UserVerificationRequirement, + }, + }, + }) as PublicKeyCredential | null; + + if (!credential) throw new Error("クレデンシャルの作成に失敗しました"); + + const attestation = credential.response as AuthenticatorAttestationResponse; + const regResponse: RegistrationResponseJSON = { + id: credential.id, + rawId: bufferToB64url(credential.rawId), + response: { + clientDataJSON: bufferToB64url(attestation.clientDataJSON), + attestationObject: bufferToB64url(attestation.attestationObject), + transports: attestation.getTransports + ? (attestation.getTransports() as AuthenticatorTransportFuture[]) + : [], + }, + authenticatorAttachment: credential.authenticatorAttachment ?? undefined, + clientExtensionResults: credential.getClientExtensionResults(), + type: "public-key", + }; const verRes = await fetch("/auth/register?intent=verify", { method: "POST", |
