NFC
This commit is contained in:
parent
c991fe7b6d
commit
e6b5fed399
8 changed files with 6647 additions and 6623 deletions
|
|
@ -13,7 +13,8 @@ export async function POST(req: NextRequest) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
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
|
// Returns { session_id, challenge } — browser completes the WebAuthn step
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const ENROLLMENT_OPEN = process.env.ENROLLMENT_OPEN === "true";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
if (!ENROLLMENT_OPEN) {
|
||||||
|
return new NextResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
|
||||||
const res = await fetch("http://localhost:3001/auth/register/finish", {
|
const res = await fetch("http://localhost:3001/auth/register/finish", {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const ENROLLMENT_OPEN = process.env.ENROLLMENT_OPEN === "true";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
if (!ENROLLMENT_OPEN) {
|
||||||
|
return new NextResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
const { username, password } = await req.json();
|
const { username, password } = await req.json();
|
||||||
|
|
||||||
const res = await fetch("http://localhost:3001/auth/register/start", {
|
const res = await fetch("http://localhost:3001/auth/register/start", {
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,13 @@ export default function AuthPage() {
|
||||||
setStatus("waiting_yubikey");
|
setStatus("waiting_yubikey");
|
||||||
const opts = challenge.publicKey;
|
const opts = challenge.publicKey;
|
||||||
opts.challenge = b64uToBuf(opts.challenge);
|
opts.challenge = b64uToBuf(opts.challenge);
|
||||||
|
opts.userVerification = "discouraged";
|
||||||
if (opts.allowCredentials) {
|
if (opts.allowCredentials) {
|
||||||
opts.allowCredentials = opts.allowCredentials.map(
|
opts.allowCredentials = opts.allowCredentials.map(
|
||||||
(c: { id: string; type: string; transports?: string[] }) => ({
|
(c: { id: string; type: string; transports?: string[] }) => ({
|
||||||
...c,
|
type: c.type,
|
||||||
id: b64uToBuf(c.id),
|
id: b64uToBuf(c.id),
|
||||||
|
transports: ["usb", "nfc", "ble", "hybrid"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,14 @@ export default function EnrollPage() {
|
||||||
const opts = challenge.publicKey;
|
const opts = challenge.publicKey;
|
||||||
opts.challenge = b64uToBuf(opts.challenge);
|
opts.challenge = b64uToBuf(opts.challenge);
|
||||||
opts.user.id = b64uToBuf(opts.user.id);
|
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) {
|
if (opts.excludeCredentials) {
|
||||||
opts.excludeCredentials = opts.excludeCredentials.map(
|
opts.excludeCredentials = opts.excludeCredentials.map(
|
||||||
(c: { id: string; type: string; transports?: string[] }) => ({
|
(c: { id: string; type: string; transports?: string[] }) => ({
|
||||||
|
|
@ -89,9 +97,7 @@ export default function EnrollPage() {
|
||||||
response: {
|
response: {
|
||||||
attestationObject: bufToB64u(attestation.attestationObject),
|
attestationObject: bufToB64u(attestation.attestationObject),
|
||||||
clientDataJSON: bufToB64u(attestation.clientDataJSON),
|
clientDataJSON: bufToB64u(attestation.clientDataJSON),
|
||||||
transports: attestation.getTransports
|
transports: ["usb", "nfc", "ble", "hybrid"],
|
||||||
? attestation.getTransports()
|
|
||||||
: [],
|
|
||||||
},
|
},
|
||||||
extensions: {},
|
extensions: {},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,12 @@ export default function RootLayout({
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${dmSans.variable} ${playfair.variable} h-full antialiased`}
|
className={`${dmSans.variable} ${playfair.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full h-full flex flex-col">{children}</body>
|
<body className="min-h-full h-full flex flex-col">
|
||||||
|
{children}
|
||||||
|
<span className="fixed bottom-3 right-4 text-[10px] text-gray-300 select-none pointer-events-none">
|
||||||
|
v0.1.0
|
||||||
|
</span>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
export function proxy(req: NextRequest) {
|
const ENROLLMENT_OPEN = process.env.ENROLLMENT_OPEN === "true";
|
||||||
const token = req.cookies.get("token")?.value;
|
|
||||||
|
export function middleware(req: NextRequest) {
|
||||||
const { pathname } = req.nextUrl;
|
const { pathname } = req.nextUrl;
|
||||||
|
|
||||||
// always allow login page and auth api routes
|
// Enrollment routes — only accessible when enrollment is open
|
||||||
|
if (
|
||||||
|
pathname.startsWith("/enroll") ||
|
||||||
|
pathname.startsWith("/api/auth/register")
|
||||||
|
) {
|
||||||
|
return ENROLLMENT_OPEN
|
||||||
|
? NextResponse.next()
|
||||||
|
: new NextResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always allow login page and auth api routes
|
||||||
if (pathname.startsWith("/auth") || pathname.startsWith("/api/auth")) {
|
if (pathname.startsWith("/auth") || pathname.startsWith("/api/auth")) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// no token — redirect to login
|
// No token — redirect to login
|
||||||
|
const token = req.cookies.get("token")?.value;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
const loginUrl = new URL("/auth", req.url);
|
const loginUrl = new URL("/auth", req.url);
|
||||||
loginUrl.searchParams.set("callbackUrl", pathname);
|
loginUrl.searchParams.set("callbackUrl", pathname);
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -68,7 +68,6 @@
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
|
|
@ -1563,7 +1562,6 @@
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1623,7 +1621,6 @@
|
||||||
"integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
|
"integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.57.2",
|
"@typescript-eslint/scope-manager": "8.57.2",
|
||||||
"@typescript-eslint/types": "8.57.2",
|
"@typescript-eslint/types": "8.57.2",
|
||||||
|
|
@ -2149,7 +2146,6 @@
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -2493,7 +2489,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -3062,7 +3057,6 @@
|
||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -3248,7 +3242,6 @@
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|
@ -5446,7 +5439,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -5456,7 +5448,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|
@ -6154,7 +6145,6 @@
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -6317,7 +6307,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -6593,7 +6582,6 @@
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue