Berkkirik commited on
Commit
2bc1ea3
·
1 Parent(s): 0e06155

auth: HTTP Basic Auth (UI_USERNAME + UI_PASSWORD)

Browse files
Files changed (1) hide show
  1. middleware.ts +21 -18
middleware.ts CHANGED
@@ -1,28 +1,26 @@
1
  /**
2
- * HTTP Basic Auth gate in front of the entire UI.
3
  *
4
- * Why: the underlying HF Space is going Public so non-org users can reach the
5
- * URL, but BSS docs and the inference API still need a single shared
6
- * credential. The password is read from the server-only env var
7
- * `UI_PASSWORD` (set as an HF Space secret) — never inlined into the bundle.
8
  *
9
- * The browser handles the credential prompt natively no login page UI to
10
- * design. Users authenticate once per session; refreshes carry the cached
11
- * credentials transparently.
12
  */
13
  import { NextRequest, NextResponse } from "next/server";
14
 
15
  export const config = {
16
- // Run on every route except Next's internals and static assets.
17
- matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
18
  };
19
 
20
  const REALM = 'Basic realm="Etiya BSS Atelier", charset="UTF-8"';
21
 
22
  export function middleware(req: NextRequest) {
23
  const password = process.env.UI_PASSWORD;
24
- // No password configured → fail open. Lets local dev work without setup,
25
- // and prevents an unconfigured deploy from looking broken.
26
  if (!password) return NextResponse.next();
27
 
28
  const auth = req.headers.get("authorization");
@@ -30,16 +28,23 @@ export function middleware(req: NextRequest) {
30
  return unauthorised();
31
  }
32
 
 
33
  let pass = "";
34
  try {
35
  const decoded = atob(auth.slice("Basic ".length));
36
  const colon = decoded.indexOf(":");
37
- pass = colon >= 0 ? decoded.slice(colon + 1) : decoded;
 
 
 
38
  } catch {
39
  return unauthorised();
40
  }
41
 
42
- if (!constantTimeEqual(pass, password)) {
 
 
 
43
  return unauthorised();
44
  }
45
  return NextResponse.next();
@@ -51,14 +56,12 @@ function unauthorised() {
51
  headers: {
52
  "WWW-Authenticate": REALM,
53
  "Content-Type": "text/plain; charset=utf-8",
 
 
54
  },
55
  });
56
  }
57
 
58
- /**
59
- * Constant-time string comparison — avoids timing leaks even though we're
60
- * gating a single shared secret. Cheap insurance.
61
- */
62
  function constantTimeEqual(a: string, b: string): boolean {
63
  if (a.length !== b.length) return false;
64
  let diff = 0;
 
1
  /**
2
+ * HTTP Basic Auth gate for the entire UI.
3
  *
4
+ * Username is read from `UI_USERNAME` (defaults to "etiya") and password from
5
+ * `UI_PASSWORD`. Both are server-only env vars set as HF Space secrets they
6
+ * are never inlined into the client bundle.
 
7
  *
8
+ * The browser's native credentials prompt handles the UX. After the user
9
+ * authenticates once, the browser caches the credentials per-origin and
10
+ * sends them automatically on every subsequent request to this realm.
11
  */
12
  import { NextRequest, NextResponse } from "next/server";
13
 
14
  export const config = {
15
+ matcher: ["/((?!_next/static|_next/image|_next/data|favicon.ico).*)"],
 
16
  };
17
 
18
  const REALM = 'Basic realm="Etiya BSS Atelier", charset="UTF-8"';
19
 
20
  export function middleware(req: NextRequest) {
21
  const password = process.env.UI_PASSWORD;
22
+ const username = process.env.UI_USERNAME || "etiya";
23
+ // Fail-open if no password configured useful for local dev with no setup.
24
  if (!password) return NextResponse.next();
25
 
26
  const auth = req.headers.get("authorization");
 
28
  return unauthorised();
29
  }
30
 
31
+ let user = "";
32
  let pass = "";
33
  try {
34
  const decoded = atob(auth.slice("Basic ".length));
35
  const colon = decoded.indexOf(":");
36
+ if (colon >= 0) {
37
+ user = decoded.slice(0, colon);
38
+ pass = decoded.slice(colon + 1);
39
+ }
40
  } catch {
41
  return unauthorised();
42
  }
43
 
44
+ if (
45
+ !constantTimeEqual(user, username) ||
46
+ !constantTimeEqual(pass, password)
47
+ ) {
48
  return unauthorised();
49
  }
50
  return NextResponse.next();
 
56
  headers: {
57
  "WWW-Authenticate": REALM,
58
  "Content-Type": "text/plain; charset=utf-8",
59
+ // Discourage caching the 401 response so re-prompts behave predictably.
60
+ "Cache-Control": "no-store",
61
  },
62
  });
63
  }
64
 
 
 
 
 
65
  function constantTimeEqual(a: string, b: string): boolean {
66
  if (a.length !== b.length) return false;
67
  let diff = 0;