zofiasmolenasana commited on
Commit
3195abb
Β·
unverified Β·
1 Parent(s): b021872

Auth: Set-Cookie session + credentials include (HF strips Authorization on GET)

Browse files
Files changed (4) hide show
  1. app.py +21 -6
  2. static/index.html +11 -0
  3. static/rag_eval.html +2 -2
  4. static/train.html +2 -2
app.py CHANGED
@@ -20,7 +20,7 @@ from datetime import datetime, timezone
20
  from pathlib import Path
21
  from typing import Any, Optional
22
 
23
- from fastapi import FastAPI, HTTPException, Query, Depends, Cookie, Header
24
  from fastapi.staticfiles import StaticFiles
25
  from fastapi.responses import FileResponse, JSONResponse
26
  from pydantic import BaseModel
@@ -174,13 +174,26 @@ class AuthPayload(BaseModel):
174
 
175
 
176
  @app.post("/api/auth")
177
- def authenticate(payload: AuthPayload):
178
- """Verify password and return a signed session token (survives server restarts)."""
 
 
 
 
179
  if not config.APP_PASSWORD:
180
  return {"ok": True, "token": ""}
181
  if not hmac.compare_digest(payload.password, config.APP_PASSWORD):
182
  raise HTTPException(status_code=403, detail="Wrong password")
183
- return {"ok": True, "token": _issue_signed_session_token()}
 
 
 
 
 
 
 
 
 
184
 
185
 
186
  @app.get("/api/auth/mode")
@@ -200,8 +213,10 @@ def auth_check(authorization: Optional[str] = Header(None), session: str = Cooki
200
 
201
  def _extract_token(authorization: Optional[str] = None, session: Optional[str] = None) -> Optional[str]:
202
  if authorization and authorization.startswith("Bearer "):
203
- return authorization[7:]
204
- return session
 
 
205
 
206
 
207
  def _require_auth(authorization: Optional[str] = Header(None), session: str = Cookie(None)):
 
20
  from pathlib import Path
21
  from typing import Any, Optional
22
 
23
+ from fastapi import FastAPI, HTTPException, Query, Depends, Cookie, Header, Response
24
  from fastapi.staticfiles import StaticFiles
25
  from fastapi.responses import FileResponse, JSONResponse
26
  from pydantic import BaseModel
 
174
 
175
 
176
  @app.post("/api/auth")
177
+ def authenticate(payload: AuthPayload, response: Response):
178
+ """Verify password and return a signed session token (survives server restarts).
179
+
180
+ Also sets an HttpOnly ``session`` cookie so GET requests work when a reverse proxy
181
+ strips the ``Authorization`` header (common on Hugging Face Spaces).
182
+ """
183
  if not config.APP_PASSWORD:
184
  return {"ok": True, "token": ""}
185
  if not hmac.compare_digest(payload.password, config.APP_PASSWORD):
186
  raise HTTPException(status_code=403, detail="Wrong password")
187
+ token = _issue_signed_session_token()
188
+ response.set_cookie(
189
+ key="session",
190
+ value=token,
191
+ max_age=_SESSION_TTL_SEC,
192
+ httponly=True,
193
+ samesite="lax",
194
+ path="/",
195
+ )
196
+ return {"ok": True, "token": token}
197
 
198
 
199
  @app.get("/api/auth/mode")
 
213
 
214
  def _extract_token(authorization: Optional[str] = None, session: Optional[str] = None) -> Optional[str]:
215
  if authorization and authorization.startswith("Bearer "):
216
+ t = authorization[7:].strip()
217
+ if t:
218
+ return t
219
+ return session if session else None
220
 
221
 
222
  def _require_auth(authorization: Optional[str] = Header(None), session: str = Cookie(None)):
static/index.html CHANGED
@@ -324,6 +324,17 @@
324
  <script>
325
  (function(){
326
 
 
 
 
 
 
 
 
 
 
 
 
327
  /* ── i18n ───────────────────────────────────────────────────────────── */
328
  const I18N = {
329
  en: {
 
324
  <script>
325
  (function(){
326
 
327
+ /* Ensure cookies (session) are sent on /api/* β€” HF proxy may strip Authorization on GET. */
328
+ const _nativeFetch = window.fetch;
329
+ window.fetch = function(input, init) {
330
+ init = init ? {...init} : {};
331
+ const u = typeof input === "string" ? input : (input && input.url) || "";
332
+ if (u.startsWith("/api") && init.credentials === undefined) {
333
+ init.credentials = "include";
334
+ }
335
+ return _nativeFetch.call(window, input, init);
336
+ };
337
+
338
  /* ── i18n ───────────────────────────────────────────────────────────── */
339
  const I18N = {
340
  en: {
static/rag_eval.html CHANGED
@@ -314,7 +314,7 @@ async function api(path, opts={}) {
314
  headers['Content-Type'] = 'application/json';
315
  opts.body = JSON.stringify(opts.body);
316
  }
317
- const res = await fetch(path, {...opts, headers});
318
  if (res.status === 401) { showLogin(); throw new Error('Auth required'); }
319
  if (!res.ok) {
320
  const err = await res.json().catch(() => ({detail: res.statusText}));
@@ -329,7 +329,7 @@ function hideLogin() { document.getElementById('login-overlay').style.display =
329
  async function doLogin() {
330
  const pw = document.getElementById('login-pw').value;
331
  try {
332
- const r = await fetch('/api/auth', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw})});
333
  const d = await r.json();
334
  if (d.ok) { TOKEN = d.token; localStorage.setItem('auth_token', TOKEN); hideLogin(); loadSheets(); }
335
  else { document.getElementById('login-error').style.display='block'; }
 
314
  headers['Content-Type'] = 'application/json';
315
  opts.body = JSON.stringify(opts.body);
316
  }
317
+ const res = await fetch(path, {...opts, headers, credentials: "include"});
318
  if (res.status === 401) { showLogin(); throw new Error('Auth required'); }
319
  if (!res.ok) {
320
  const err = await res.json().catch(() => ({detail: res.statusText}));
 
329
  async function doLogin() {
330
  const pw = document.getElementById('login-pw').value;
331
  try {
332
+ const r = await fetch('/api/auth', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw}), credentials:'include'});
333
  const d = await r.json();
334
  if (d.ok) { TOKEN = d.token; localStorage.setItem('auth_token', TOKEN); hideLogin(); loadSheets(); }
335
  else { document.getElementById('login-error').style.display='block'; }
static/train.html CHANGED
@@ -517,11 +517,11 @@ function headers() {
517
  }
518
 
519
  async function api(path, opts = {}) {
520
- const res = await fetch(path, {headers: headers(), ...opts});
521
  if (res.status === 401) {
522
  const pw = prompt('Enter password:');
523
  if (!pw) return null;
524
- const auth = await fetch('/api/auth', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw})});
525
  if (auth.ok) { const d = await auth.json(); token = d.token; localStorage.setItem('labeler_token', token); return api(path, opts); }
526
  return null;
527
  }
 
517
  }
518
 
519
  async function api(path, opts = {}) {
520
+ const res = await fetch(path, {headers: headers(), credentials: 'include', ...opts});
521
  if (res.status === 401) {
522
  const pw = prompt('Enter password:');
523
  if (!pw) return null;
524
+ const auth = await fetch('/api/auth', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw}), credentials:'include'});
525
  if (auth.ok) { const d = await auth.json(); token = d.token; localStorage.setItem('labeler_token', token); return api(path, opts); }
526
  return null;
527
  }