d2l-ui / middleware.ts
Berkkirik's picture
auth: HTTP Basic Auth (UI_USERNAME + UI_PASSWORD)
2bc1ea3
/**
* HTTP Basic Auth gate for the entire UI.
*
* Username is read from `UI_USERNAME` (defaults to "etiya") and password from
* `UI_PASSWORD`. Both are server-only env vars set as HF Space secrets — they
* are never inlined into the client bundle.
*
* The browser's native credentials prompt handles the UX. After the user
* authenticates once, the browser caches the credentials per-origin and
* sends them automatically on every subsequent request to this realm.
*/
import { NextRequest, NextResponse } from "next/server";
export const config = {
matcher: ["/((?!_next/static|_next/image|_next/data|favicon.ico).*)"],
};
const REALM = 'Basic realm="Etiya BSS Atelier", charset="UTF-8"';
export function middleware(req: NextRequest) {
const password = process.env.UI_PASSWORD;
const username = process.env.UI_USERNAME || "etiya";
// Fail-open if no password configured — useful for local dev with no setup.
if (!password) return NextResponse.next();
const auth = req.headers.get("authorization");
if (!auth?.startsWith("Basic ")) {
return unauthorised();
}
let user = "";
let pass = "";
try {
const decoded = atob(auth.slice("Basic ".length));
const colon = decoded.indexOf(":");
if (colon >= 0) {
user = decoded.slice(0, colon);
pass = decoded.slice(colon + 1);
}
} catch {
return unauthorised();
}
if (
!constantTimeEqual(user, username) ||
!constantTimeEqual(pass, password)
) {
return unauthorised();
}
return NextResponse.next();
}
function unauthorised() {
return new NextResponse("Authentication required.", {
status: 401,
headers: {
"WWW-Authenticate": REALM,
"Content-Type": "text/plain; charset=utf-8",
// Discourage caching the 401 response so re-prompts behave predictably.
"Cache-Control": "no-store",
},
});
}
function constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}