summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/routes/auth.register.tsx64
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",