Spaces:
Sleeping
Sleeping
Commit ·
45a6b1d
1
Parent(s): d70361b
Fix Hugging Face login cookies so new accounts can stay signed in.
Browse filesUse Secure + SameSite=None on real HF Spaces (iframe-safe), persist a JWT secret across restarts when SECRET_KEY is unset, and surface a cookie-block error instead of a silent bounce to /login.
Co-authored-by: Cursor <cursoragent@cursor.com>
- .gitignore +1 -0
- app/auth.py +52 -26
- app/main.py +8 -2
- static/js/dda/login.js +18 -0
- templates/login_dda.html +1 -1
.gitignore
CHANGED
|
@@ -19,6 +19,7 @@ data/library_sources/**/*.jpg
|
|
| 19 |
data/library_sources/**/*.jpeg
|
| 20 |
!library_sources/**/.gitkeep
|
| 21 |
.env
|
|
|
|
| 22 |
*.egg-info/
|
| 23 |
dist/
|
| 24 |
build/
|
|
|
|
| 19 |
data/library_sources/**/*.jpeg
|
| 20 |
!library_sources/**/.gitkeep
|
| 21 |
.env
|
| 22 |
+
data/.secret_key
|
| 23 |
*.egg-info/
|
| 24 |
dist/
|
| 25 |
build/
|
app/auth.py
CHANGED
|
@@ -17,23 +17,50 @@ from .models import User, LoginHistory, PasswordResetToken, LoginOtp
|
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
ALGORITHM = "HS256"
|
| 38 |
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
| 39 |
COOKIE_NAME = "satellite_token"
|
|
@@ -55,15 +82,14 @@ if _COOKIE_SECURE_ENV in ("1", "true", "yes"):
|
|
| 55 |
elif _COOKIE_SECURE_ENV in ("0", "false", "no"):
|
| 56 |
IS_SECURE_COOKIE = False
|
| 57 |
else:
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
IS_SECURE_COOKIE = bool(os.environ.get("RENDER"))
|
| 67 |
|
| 68 |
|
| 69 |
def get_client_ip(request: Request) -> str:
|
|
|
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
+
|
| 21 |
+
def _is_huggingface_space() -> bool:
|
| 22 |
+
"""True on a real HF Space, not local `.env` SPACE_ID=local-dev/..."""
|
| 23 |
+
sid = os.environ.get("SPACE_ID", "").strip().lower()
|
| 24 |
+
return bool(sid) and not sid.startswith("local")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _secret_key_file():
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
if _is_huggingface_space():
|
| 30 |
+
return Path.home() / "data" / ".secret_key"
|
| 31 |
+
return Path(__file__).resolve().parent.parent / "data" / ".secret_key"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _load_or_create_secret() -> str:
|
| 35 |
+
env_secret = os.environ.get("SECRET_KEY", "").strip()
|
| 36 |
+
if env_secret:
|
| 37 |
+
return env_secret
|
| 38 |
+
path = _secret_key_file()
|
| 39 |
+
try:
|
| 40 |
+
existing = path.read_text(encoding="utf-8").strip()
|
| 41 |
+
if existing:
|
| 42 |
+
return existing
|
| 43 |
+
except OSError:
|
| 44 |
+
pass
|
| 45 |
+
secret = secrets.token_hex(32)
|
| 46 |
+
try:
|
| 47 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
path.write_text(secret, encoding="utf-8")
|
| 49 |
+
logger.warning(
|
| 50 |
+
"SECRET_KEY env var not set — persisted a generated secret to %s "
|
| 51 |
+
"so logins survive process restarts. Set SECRET_KEY in the Space "
|
| 52 |
+
"secrets for a rebuild-safe value.",
|
| 53 |
+
path,
|
| 54 |
+
)
|
| 55 |
+
except OSError:
|
| 56 |
+
logger.warning(
|
| 57 |
+
"SECRET_KEY env var not set — generated a one-time secret for this "
|
| 58 |
+
"process (sessions die on restart). Set SECRET_KEY in the environment."
|
| 59 |
+
)
|
| 60 |
+
return secret
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
SECRET_KEY = _load_or_create_secret()
|
| 64 |
ALGORITHM = "HS256"
|
| 65 |
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
| 66 |
COOKIE_NAME = "satellite_token"
|
|
|
|
| 82 |
elif _COOKIE_SECURE_ENV in ("0", "false", "no"):
|
| 83 |
IS_SECURE_COOKIE = False
|
| 84 |
else:
|
| 85 |
+
# Local `.env` uses SPACE_ID=local-dev/... over HTTP — must stay insecure.
|
| 86 |
+
# Real Hugging Face Spaces are HTTPS and often run inside huggingface.co
|
| 87 |
+
# iframe, which drops SameSite=Lax cookies unless they are Secure + None.
|
| 88 |
+
IS_SECURE_COOKIE = bool(os.environ.get("RENDER")) or _is_huggingface_space()
|
| 89 |
+
|
| 90 |
+
# SameSite=None is required for the Hugging Face embed iframe; it also
|
| 91 |
+
# requires Secure=True. First-party local HTTP keeps Lax.
|
| 92 |
+
COOKIE_SAMESITE = "none" if IS_SECURE_COOKIE else "lax"
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
def get_client_ip(request: Request) -> str:
|
app/main.py
CHANGED
|
@@ -28,6 +28,7 @@ from PIL import Image, ImageOps
|
|
| 28 |
from .auth import (
|
| 29 |
ALGORITHM,
|
| 30 |
COOKIE_NAME,
|
|
|
|
| 31 |
IS_SECURE_COOKIE,
|
| 32 |
LOGIN_OTP_EXPIRE_MINUTES,
|
| 33 |
LOGIN_OTP_RESEND_SECONDS,
|
|
@@ -383,7 +384,7 @@ def _auth_response(token: str, user: User, remember: bool = False):
|
|
| 383 |
key=COOKIE_NAME,
|
| 384 |
value=token,
|
| 385 |
httponly=True,
|
| 386 |
-
samesite=
|
| 387 |
secure=IS_SECURE_COOKIE,
|
| 388 |
path="/",
|
| 389 |
)
|
|
@@ -517,7 +518,12 @@ def logout(request: Request, db: Session = Depends(get_db)):
|
|
| 517 |
ip_address=get_client_ip(request), user_agent=get_user_agent(request),
|
| 518 |
)
|
| 519 |
response = JSONResponse(content={"ok": True})
|
| 520 |
-
response.delete_cookie(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
return response
|
| 522 |
|
| 523 |
|
|
|
|
| 28 |
from .auth import (
|
| 29 |
ALGORITHM,
|
| 30 |
COOKIE_NAME,
|
| 31 |
+
COOKIE_SAMESITE,
|
| 32 |
IS_SECURE_COOKIE,
|
| 33 |
LOGIN_OTP_EXPIRE_MINUTES,
|
| 34 |
LOGIN_OTP_RESEND_SECONDS,
|
|
|
|
| 384 |
key=COOKIE_NAME,
|
| 385 |
value=token,
|
| 386 |
httponly=True,
|
| 387 |
+
samesite=COOKIE_SAMESITE,
|
| 388 |
secure=IS_SECURE_COOKIE,
|
| 389 |
path="/",
|
| 390 |
)
|
|
|
|
| 518 |
ip_address=get_client_ip(request), user_agent=get_user_agent(request),
|
| 519 |
)
|
| 520 |
response = JSONResponse(content={"ok": True})
|
| 521 |
+
response.delete_cookie(
|
| 522 |
+
COOKIE_NAME,
|
| 523 |
+
path="/",
|
| 524 |
+
secure=IS_SECURE_COOKIE,
|
| 525 |
+
samesite=COOKIE_SAMESITE,
|
| 526 |
+
)
|
| 527 |
return response
|
| 528 |
|
| 529 |
|
static/js/dda/login.js
CHANGED
|
@@ -162,6 +162,15 @@
|
|
| 162 |
await ddaApi('POST', '/api/auth/login/verify-otp', {
|
| 163 |
body: JSON.stringify({ otpToken: pendingOtp?.token, code }),
|
| 164 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
window.location.href = '/';
|
| 166 |
} catch (err) {
|
| 167 |
showDdaError(err.message || 'Invalid sign-in code.');
|
|
@@ -294,6 +303,15 @@
|
|
| 294 |
remember_me,
|
| 295 |
}),
|
| 296 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
window.location.href = '/';
|
| 298 |
return;
|
| 299 |
}
|
|
|
|
| 162 |
await ddaApi('POST', '/api/auth/login/verify-otp', {
|
| 163 |
body: JSON.stringify({ otpToken: pendingOtp?.token, code }),
|
| 164 |
});
|
| 165 |
+
try {
|
| 166 |
+
await ddaApi('GET', '/api/me');
|
| 167 |
+
} catch (_) {
|
| 168 |
+
showDdaError(
|
| 169 |
+
'Sign-in succeeded, but this browser blocked the session cookie. Open the app in its own tab (not the Hugging Face embed): '
|
| 170 |
+
+ window.location.origin,
|
| 171 |
+
);
|
| 172 |
+
return;
|
| 173 |
+
}
|
| 174 |
window.location.href = '/';
|
| 175 |
} catch (err) {
|
| 176 |
showDdaError(err.message || 'Invalid sign-in code.');
|
|
|
|
| 303 |
remember_me,
|
| 304 |
}),
|
| 305 |
});
|
| 306 |
+
try {
|
| 307 |
+
await ddaApi('GET', '/api/me');
|
| 308 |
+
} catch (_) {
|
| 309 |
+
showDdaError(
|
| 310 |
+
'Sign-in succeeded, but this browser blocked the session cookie. Open the app in its own tab (not the Hugging Face embed): '
|
| 311 |
+
+ window.location.origin,
|
| 312 |
+
);
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
window.location.href = '/';
|
| 316 |
return;
|
| 317 |
}
|
templates/login_dda.html
CHANGED
|
@@ -132,6 +132,6 @@
|
|
| 132 |
|
| 133 |
<script src="/static/js/dda/shared.js?v=4"></script>
|
| 134 |
<script src="/static/js/dda/countries.js?v=1"></script>
|
| 135 |
-
<script src="/static/js/dda/login.js?v=
|
| 136 |
</body>
|
| 137 |
</html>
|
|
|
|
| 132 |
|
| 133 |
<script src="/static/js/dda/shared.js?v=4"></script>
|
| 134 |
<script src="/static/js/dda/countries.js?v=1"></script>
|
| 135 |
+
<script src="/static/js/dda/login.js?v=11"></script>
|
| 136 |
</body>
|
| 137 |
</html>
|