Spaces:
Sleeping
Sleeping
Commit ·
2f83d3e
1
Parent(s): 68e5370
Wire app.py/secrets/requirements to the new cookie-based token store
Browse filesCompletes the previous commit -- these didn't get staged there because
git add aborted when one of the paths in that batch (the deleted
token_store.py) no longer existed. app.py now imports
cookie_token_store instead of token_store; secrets.toml/bootstrap_secrets.py
use a [cookies] section instead of [supabase]; requirements.txt swaps
psycopg2-binary for extra-streamlit-components.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- README.md +12 -12
- app.py +6 -6
- requirements.txt +1 -1
- src/bootstrap_secrets.py +2 -3
README.md
CHANGED
|
@@ -43,11 +43,16 @@ button) only ever handles identity -- it deliberately never exposes a
|
|
| 43 |
logged in, a separate one-time **Connect Google Drive** button appears in the
|
| 44 |
sidebar; clicking it runs its own OAuth exchange (`access_type=offline` +
|
| 45 |
`prompt=consent`) to force Google to hand back a `refresh_token`, which gets
|
| 46 |
-
encrypted and stored in a
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
### One-time Google Cloud setup
|
| 53 |
1. Create an OAuth 2.0 **Web application** Client ID; add your app's URL(s)
|
|
@@ -73,14 +78,9 @@ client_id = "<oauth client id>"
|
|
| 73 |
client_secret = "<oauth client secret>"
|
| 74 |
server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration"
|
| 75 |
|
| 76 |
-
[
|
| 77 |
-
db_url = "postgresql://..." # Supabase connection string
|
| 78 |
encryption_key = "<Fernet.generate_key() output>"
|
| 79 |
```
|
| 80 |
-
The `[supabase]` table (`user_refresh_tokens`) is created automatically on
|
| 81 |
-
first use -- no manual migration needed. Rotate the DB password in Supabase's
|
| 82 |
-
dashboard if it's ever been pasted somewhere outside a secrets file (e.g.
|
| 83 |
-
chat, a ticket, a terminal history you don't control).
|
| 84 |
|
| 85 |
### Hugging Face Space deploy
|
| 86 |
`st.login()` only reads `st.secrets`, so `src/bootstrap_secrets.py` assembles
|
|
@@ -89,4 +89,4 @@ Variables and secrets), so nothing sensitive needs to live in the repo or the
|
|
| 89 |
image: `AUTH_CLIENT_ID`, `AUTH_CLIENT_SECRET`, `AUTH_COOKIE_SECRET`,
|
| 90 |
`AUTH_REDIRECT_URI` (your Space's public URL **with `/oauth2callback`
|
| 91 |
appended**, e.g. `https://randomcatlover-boardgames-tracker.hf.space/oauth2callback`),
|
| 92 |
-
`
|
|
|
|
| 43 |
logged in, a separate one-time **Connect Google Drive** button appears in the
|
| 44 |
sidebar; clicking it runs its own OAuth exchange (`access_type=offline` +
|
| 45 |
`prompt=consent`) to force Google to hand back a `refresh_token`, which gets
|
| 46 |
+
encrypted and stored in a long-lived browser cookie (via
|
| 47 |
+
`extra-streamlit-components`' `CookieManager`, since Streamlit page code has
|
| 48 |
+
no direct access to set real HttpOnly cookies). From then on, on that same
|
| 49 |
+
browser, the app silently mints a fresh access token from the stored
|
| 50 |
+
refresh_token -- no repeat consent screen -- until it's revoked (e.g. you
|
| 51 |
+
remove the app's access in your Google account) or the browser's cookies are
|
| 52 |
+
cleared. This is per-browser/device, not per-account centrally: a new device
|
| 53 |
+
or browser needs to click Connect Drive once, same as the very first time.
|
| 54 |
+
The cookie only ever holds the encrypted blob; the decryption key lives
|
| 55 |
+
purely in server-side secrets and never reaches the browser.
|
| 56 |
|
| 57 |
### One-time Google Cloud setup
|
| 58 |
1. Create an OAuth 2.0 **Web application** Client ID; add your app's URL(s)
|
|
|
|
| 78 |
client_secret = "<oauth client secret>"
|
| 79 |
server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration"
|
| 80 |
|
| 81 |
+
[cookies]
|
|
|
|
| 82 |
encryption_key = "<Fernet.generate_key() output>"
|
| 83 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
### Hugging Face Space deploy
|
| 86 |
`st.login()` only reads `st.secrets`, so `src/bootstrap_secrets.py` assembles
|
|
|
|
| 89 |
image: `AUTH_CLIENT_ID`, `AUTH_CLIENT_SECRET`, `AUTH_COOKIE_SECRET`,
|
| 90 |
`AUTH_REDIRECT_URI` (your Space's public URL **with `/oauth2callback`
|
| 91 |
appended**, e.g. `https://randomcatlover-boardgames-tracker.hf.space/oauth2callback`),
|
| 92 |
+
`COOKIE_ENCRYPTION_KEY`.
|
app.py
CHANGED
|
@@ -11,8 +11,8 @@ ensure_secrets_file()
|
|
| 11 |
|
| 12 |
import streamlit as st
|
| 13 |
|
|
|
|
| 14 |
import google_oauth
|
| 15 |
-
import token_store
|
| 16 |
from analytics import inject_umami
|
| 17 |
|
| 18 |
st.set_page_config(page_title="Board Game Tracker", layout="wide")
|
|
@@ -64,8 +64,8 @@ def _handle_drive_oauth_callback() -> None:
|
|
| 64 |
return
|
| 65 |
|
| 66 |
refresh_token = tokens.get("refresh_token")
|
| 67 |
-
if refresh_token and "
|
| 68 |
-
|
| 69 |
_store_access_token(tokens)
|
| 70 |
st.query_params.clear()
|
| 71 |
st.rerun()
|
|
@@ -79,8 +79,8 @@ def _ensure_drive_access() -> None:
|
|
| 79 |
if not st.user.is_logged_in or _access_token_valid():
|
| 80 |
return
|
| 81 |
|
| 82 |
-
if "
|
| 83 |
-
refresh_token =
|
| 84 |
if refresh_token:
|
| 85 |
try:
|
| 86 |
tokens = google_oauth.refresh_access_token(
|
|
@@ -91,7 +91,7 @@ def _ensure_drive_access() -> None:
|
|
| 91 |
_store_access_token(tokens)
|
| 92 |
return
|
| 93 |
except Exception:
|
| 94 |
-
|
| 95 |
|
| 96 |
state = uuid.uuid4().hex
|
| 97 |
st.session_state["drive_oauth_state"] = state
|
|
|
|
| 11 |
|
| 12 |
import streamlit as st
|
| 13 |
|
| 14 |
+
import cookie_token_store
|
| 15 |
import google_oauth
|
|
|
|
| 16 |
from analytics import inject_umami
|
| 17 |
|
| 18 |
st.set_page_config(page_title="Board Game Tracker", layout="wide")
|
|
|
|
| 64 |
return
|
| 65 |
|
| 66 |
refresh_token = tokens.get("refresh_token")
|
| 67 |
+
if refresh_token and "cookies" in st.secrets:
|
| 68 |
+
cookie_token_store.save_refresh_token(st.user.email, refresh_token)
|
| 69 |
_store_access_token(tokens)
|
| 70 |
st.query_params.clear()
|
| 71 |
st.rerun()
|
|
|
|
| 79 |
if not st.user.is_logged_in or _access_token_valid():
|
| 80 |
return
|
| 81 |
|
| 82 |
+
if "cookies" in st.secrets:
|
| 83 |
+
refresh_token = cookie_token_store.get_refresh_token(st.user.email)
|
| 84 |
if refresh_token:
|
| 85 |
try:
|
| 86 |
tokens = google_oauth.refresh_access_token(
|
|
|
|
| 91 |
_store_access_token(tokens)
|
| 92 |
return
|
| 93 |
except Exception:
|
| 94 |
+
cookie_token_store.delete_refresh_token(st.user.email)
|
| 95 |
|
| 96 |
state = uuid.uuid4().hex
|
| 97 |
st.session_state["drive_oauth_state"] = state
|
requirements.txt
CHANGED
|
@@ -6,6 +6,6 @@ google-auth
|
|
| 6 |
Authlib
|
| 7 |
httpx
|
| 8 |
tomli_w
|
| 9 |
-
psycopg2-binary
|
| 10 |
cryptography
|
| 11 |
requests
|
|
|
|
|
|
| 6 |
Authlib
|
| 7 |
httpx
|
| 8 |
tomli_w
|
|
|
|
| 9 |
cryptography
|
| 10 |
requests
|
| 11 |
+
extra-streamlit-components
|
src/bootstrap_secrets.py
CHANGED
|
@@ -19,9 +19,8 @@ def ensure_secrets_file() -> None:
|
|
| 19 |
"client_secret": os.environ["AUTH_CLIENT_SECRET"],
|
| 20 |
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
| 21 |
},
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"encryption_key": os.environ["SUPABASE_ENCRYPTION_KEY"],
|
| 25 |
},
|
| 26 |
}
|
| 27 |
_SECRETS_PATH.write_text(tomli_w.dumps(data))
|
|
|
|
| 19 |
"client_secret": os.environ["AUTH_CLIENT_SECRET"],
|
| 20 |
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
| 21 |
},
|
| 22 |
+
"cookies": {
|
| 23 |
+
"encryption_key": os.environ["COOKIE_ENCRYPTION_KEY"],
|
|
|
|
| 24 |
},
|
| 25 |
}
|
| 26 |
_SECRETS_PATH.write_text(tomli_w.dumps(data))
|