Spaces:
Sleeping
Sleeping
zofiasmolenasana commited on
Auth: Set-Cookie session + credentials include (HF strips Authorization on GET)
Browse files- app.py +21 -6
- static/index.html +11 -0
- static/rag_eval.html +2 -2
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 204 |
-
|
|
|
|
|
|
|
| 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 |
}
|