fffiloni commited on
Commit
ccd0f32
·
verified ·
1 Parent(s): 84bc1d1

Upload 4 files

Browse files
Files changed (2) hide show
  1. README.md +3 -3
  2. app.py +98 -5
README.md CHANGED
@@ -133,9 +133,9 @@ See `CHANGELOG_V194.md` for the Model Pre-scan Decision Card UI pass.
133
  See `CHANGELOG_V195.md` for the Runtime CSS Cleanup with Legacy Safety Net pass.
134
 
135
 
136
- Current release: Agentic Space Factory v198.26.11.
137
 
138
 
139
- ## v198.26.11Live Test Clarity
140
 
141
- See `CHANGELOG_V198_26_11.md`.
 
133
  See `CHANGELOG_V195.md` for the Runtime CSS Cleanup with Legacy Safety Net pass.
134
 
135
 
136
+ Current release: Agentic Space Factory v198.26.12.
137
 
138
 
139
+ ## v198.26.12OAuth Recovery
140
 
141
+ See `CHANGELOG_V198_26_12.md`.
app.py CHANGED
@@ -97,6 +97,53 @@ def _oauth_context_from_request(request: Request) -> dict[str, Any]:
97
  "warnings": oauth_warning_messages(ctx),
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def _json_error(exc: Exception) -> HTTPException:
101
  if isinstance(exc, HTTPException):
102
  return exc
@@ -548,6 +595,37 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
548
  # redirect without custom target parameters to avoid OAuth loops.
549
  return RedirectResponse("/oauth/huggingface/logout", status_code=307)
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  @fastapi_app.get("/", response_class=HTMLResponse)
552
  @fastapi_app.get("/custom", response_class=HTMLResponse)
553
  async def custom_index(): # type: ignore[no-untyped-def]
@@ -589,16 +667,24 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
589
  "custom_ui_status": "root_custom_ui",
590
  "user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", []), "auth_lifetime": ctx.get("auth_lifetime")} if ctx else None,
591
  "login_url": "/oauth/huggingface/login",
592
- "logout_url": "/oauth/huggingface/logout",
 
 
593
  "anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
594
  }
595
  )
596
 
597
  @fastapi_app.get("/api/me")
598
  async def api_me(request: Request): # type: ignore[no-untyped-def]
599
- ctx = _oauth_context_from_request(request)
 
 
 
 
 
600
  return JSONResponse(
601
  {
 
602
  "username": ctx["username"],
603
  "profile": {
604
  "name": ctx["profile"].get("name"),
@@ -614,7 +700,9 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
614
  "anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
615
  "expires_at": ctx.get("expires_at"),
616
  "login_url": "/oauth/huggingface/login",
617
- "logout_url": "/oauth/huggingface/logout",
 
 
618
  }
619
  )
620
 
@@ -723,7 +811,10 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
723
  "reason": exc.detail,
724
  "oauth_env": env_status,
725
  "login_url": "/oauth/huggingface/login",
726
- "logout_url": "/oauth/huggingface/logout",
 
 
 
727
  }
728
  )
729
  return JSONResponse(
@@ -733,7 +824,9 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
733
  "token_identity": verify_token_identity(ctx),
734
  "oauth_env": env_status,
735
  "login_url": "/oauth/huggingface/login",
736
- "logout_url": "/oauth/huggingface/logout",
 
 
737
  }
738
  )
739
 
 
97
  "warnings": oauth_warning_messages(ctx),
98
  }
99
 
100
+
101
+ def _clear_local_oauth_session(request: Request) -> dict[str, Any]:
102
+ """Clear ASF/HF OAuth session state without requiring a valid token.
103
+
104
+ This is intentionally tolerant: expired OAuth sessions must be recoverable
105
+ even when parse_huggingface_oauth/extract_oauth_context can no longer
106
+ deserialize or validate the token.
107
+ """
108
+ cleared_keys: list[str] = []
109
+ had_session = False
110
+ try:
111
+ session = request.session # type: ignore[attr-defined]
112
+ had_session = bool(session)
113
+ for key in list(session.keys()):
114
+ key_l = str(key).lower()
115
+ if "oauth" in key_l or "huggingface" in key_l or key_l in {"state", "nonce", "next", "redirect_uri"}:
116
+ cleared_keys.append(str(key))
117
+ session.clear()
118
+ except Exception:
119
+ had_session = False
120
+ return {"cleared": True, "had_session": had_session, "cleared_keys": sorted(set(cleared_keys))}
121
+
122
+
123
+ def _auth_recovery_payload(reason: str = "not_authenticated", detail: str | None = None) -> dict[str, Any]:
124
+ reason_l = (reason or "not_authenticated").lower()
125
+ if "expired" in reason_l or (detail and "expired" in detail.lower()):
126
+ reason_l = "oauth_expired"
127
+ message = "Your Hugging Face OAuth session expired. Refresh sign-in to continue."
128
+ else:
129
+ reason_l = "not_signed_in" if reason_l in {"not_authenticated", "please sign in with hugging face first."} else reason_l
130
+ message = "Sign in with Hugging Face to continue."
131
+ return {
132
+ "authenticated": False,
133
+ "reason": reason_l,
134
+ "message": message,
135
+ "login_url": "/oauth/huggingface/login",
136
+ "refresh_login_url": "/auth/refresh-login",
137
+ "reset_url": "/auth/logout-local",
138
+ "logout_url": "/auth/logout-local",
139
+ "recovery": {
140
+ "primary_action": "refresh_sign_in" if reason_l == "oauth_expired" else "sign_in",
141
+ "refresh_login_url": "/auth/refresh-login",
142
+ "reset_local_auth_url": "/auth/logout-local",
143
+ "open_in_new_tab_recommended": True,
144
+ },
145
+ }
146
+
147
  def _json_error(exc: Exception) -> HTTPException:
148
  if isinstance(exc, HTTPException):
149
  return exc
 
595
  # redirect without custom target parameters to avoid OAuth loops.
596
  return RedirectResponse("/oauth/huggingface/logout", status_code=307)
597
 
598
+ @fastapi_app.get("/auth/logout-local")
599
+ async def auth_logout_local(request: Request): # type: ignore[no-untyped-def]
600
+ # Local reset must work even when the HF OAuth token is expired and the
601
+ # official helper can no longer parse the session.
602
+ _clear_local_oauth_session(request)
603
+ response = RedirectResponse("/", status_code=303)
604
+ response.delete_cookie("session")
605
+ response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
606
+ response.headers["Pragma"] = "no-cache"
607
+ return response
608
+
609
+ @fastapi_app.get("/auth/refresh-login")
610
+ async def auth_refresh_login(request: Request): # type: ignore[no-untyped-def]
611
+ # Force a clean re-authorization path. This avoids stale oauth_info
612
+ # state after token expiry and should make Space duplication unnecessary.
613
+ _clear_local_oauth_session(request)
614
+ response = RedirectResponse("/oauth/huggingface/login", status_code=303)
615
+ response.delete_cookie("session")
616
+ response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
617
+ response.headers["Pragma"] = "no-cache"
618
+ return response
619
+
620
+ @fastapi_app.get("/auth/logout-and-refresh")
621
+ async def auth_logout_and_refresh(request: Request): # type: ignore[no-untyped-def]
622
+ _clear_local_oauth_session(request)
623
+ response = RedirectResponse("/auth/refresh-login", status_code=303)
624
+ response.delete_cookie("session")
625
+ response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
626
+ response.headers["Pragma"] = "no-cache"
627
+ return response
628
+
629
  @fastapi_app.get("/", response_class=HTMLResponse)
630
  @fastapi_app.get("/custom", response_class=HTMLResponse)
631
  async def custom_index(): # type: ignore[no-untyped-def]
 
667
  "custom_ui_status": "root_custom_ui",
668
  "user": {"username": ctx["username"], "missing_scopes": ctx.get("missing_scopes", []), "warnings": ctx.get("warnings", []), "auth_lifetime": ctx.get("auth_lifetime")} if ctx else None,
669
  "login_url": "/oauth/huggingface/login",
670
+ "refresh_login_url": "/auth/refresh-login",
671
+ "reset_auth_url": "/auth/logout-local",
672
+ "logout_url": "/auth/logout-local",
673
  "anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
674
  }
675
  )
676
 
677
  @fastapi_app.get("/api/me")
678
  async def api_me(request: Request): # type: ignore[no-untyped-def]
679
+ try:
680
+ ctx = _oauth_context_from_request(request)
681
+ except HTTPException as exc:
682
+ payload = _auth_recovery_payload(str(exc.detail), str(exc.detail))
683
+ payload["anonymous_eval"] = public_eval_config(None)
684
+ return JSONResponse(payload, status_code=401)
685
  return JSONResponse(
686
  {
687
+ "authenticated": True,
688
  "username": ctx["username"],
689
  "profile": {
690
  "name": ctx["profile"].get("name"),
 
700
  "anonymous_eval": public_eval_config(ctx["username"] if ctx else None),
701
  "expires_at": ctx.get("expires_at"),
702
  "login_url": "/oauth/huggingface/login",
703
+ "refresh_login_url": "/auth/refresh-login",
704
+ "reset_url": "/auth/logout-local",
705
+ "logout_url": "/auth/logout-local",
706
  }
707
  )
708
 
 
811
  "reason": exc.detail,
812
  "oauth_env": env_status,
813
  "login_url": "/oauth/huggingface/login",
814
+ "refresh_login_url": "/auth/refresh-login",
815
+ "reset_url": "/auth/logout-local",
816
+ "logout_url": "/auth/logout-local",
817
+ "recommended_action": "refresh_sign_in" if "expired" in str(exc.detail).lower() else "sign_in",
818
  }
819
  )
820
  return JSONResponse(
 
824
  "token_identity": verify_token_identity(ctx),
825
  "oauth_env": env_status,
826
  "login_url": "/oauth/huggingface/login",
827
+ "refresh_login_url": "/auth/refresh-login",
828
+ "reset_url": "/auth/logout-local",
829
+ "logout_url": "/auth/logout-local",
830
  }
831
  )
832