diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 0a42f0b..da92a27 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -13,7 +13,8 @@ export async function POST(req: NextRequest) { }); if (!res.ok) { - return NextResponse.json({ error: "Invalid credentials" }, { status: 401 }); + const text = await res.text(); + return NextResponse.json({ error: text }, { status: 401 }); } // Returns { session_id, challenge } — browser completes the WebAuthn step diff --git a/app/api/auth/register/finish/route.ts b/app/api/auth/register/finish/route.ts index 322c871..b82f990 100644 --- a/app/api/auth/register/finish/route.ts +++ b/app/api/auth/register/finish/route.ts @@ -1,6 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; +const ENROLLMENT_OPEN = process.env.ENROLLMENT_OPEN === "true"; + export async function POST(req: NextRequest) { + if (!ENROLLMENT_OPEN) { + return new NextResponse(null, { status: 404 }); + } const body = await req.json(); const res = await fetch("http://localhost:3001/auth/register/finish", { diff --git a/app/api/auth/register/start/route.ts b/app/api/auth/register/start/route.ts index 92ee1f6..01bf702 100644 --- a/app/api/auth/register/start/route.ts +++ b/app/api/auth/register/start/route.ts @@ -1,6 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; +const ENROLLMENT_OPEN = process.env.ENROLLMENT_OPEN === "true"; + export async function POST(req: NextRequest) { + if (!ENROLLMENT_OPEN) { + return new NextResponse(null, { status: 404 }); + } const { username, password } = await req.json(); const res = await fetch("http://localhost:3001/auth/register/start", { diff --git a/app/auth/page.tsx b/app/auth/page.tsx index c25f747..aa9aa79 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -69,11 +69,13 @@ export default function AuthPage() { setStatus("waiting_yubikey"); const opts = challenge.publicKey; opts.challenge = b64uToBuf(opts.challenge); + opts.userVerification = "discouraged"; if (opts.allowCredentials) { opts.allowCredentials = opts.allowCredentials.map( (c: { id: string; type: string; transports?: string[] }) => ({ - ...c, + type: c.type, id: b64uToBuf(c.id), + transports: ["usb", "nfc", "ble", "hybrid"], }), ); } diff --git a/app/enroll/page.tsx b/app/enroll/page.tsx index 67f65ec..c7c86c0 100644 --- a/app/enroll/page.tsx +++ b/app/enroll/page.tsx @@ -50,6 +50,14 @@ export default function EnrollPage() { const opts = challenge.publicKey; opts.challenge = b64uToBuf(opts.challenge); opts.user.id = b64uToBuf(opts.user.id); + // Force security key UI — residentKey/UV discouraged so Android + // Chrome doesn't suppress the NFC option in favour of biometrics + opts.authenticatorSelection = { + authenticatorAttachment: "cross-platform", + residentKey: "discouraged", + requireResidentKey: false, + userVerification: "discouraged", + }; if (opts.excludeCredentials) { opts.excludeCredentials = opts.excludeCredentials.map( (c: { id: string; type: string; transports?: string[] }) => ({ @@ -89,9 +97,7 @@ export default function EnrollPage() { response: { attestationObject: bufToB64u(attestation.attestationObject), clientDataJSON: bufToB64u(attestation.clientDataJSON), - transports: attestation.getTransports - ? attestation.getTransports() - : [], + transports: ["usb", "nfc", "ble", "hybrid"], }, extensions: {}, }, diff --git a/app/layout.tsx b/app/layout.tsx index 89971ae..8f58283 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -29,7 +29,12 @@ export default function RootLayout({ lang="en" className={`${dmSans.variable} ${playfair.variable} h-full antialiased`} > -
{children} + + {children} + + v0.1.0 + +