feat(backend): per-Reader userscript credential with UI install and rotation (#24)
Each Reader's userscript credential is derived from TOKEN_KEY, their Discord id and a token epoch (HMAC-SHA256, hex); only its SHA-256 sits in readers.token_sha256, so install URLs survive restarts while a database leak yields nothing but hashes. One credential authenticates the script download path and the API bearer header. - internal/token: derivation + hashing; migration 0006 adds token_epoch - seed refreshes the owner's epoch-0 hash only before first rotation - httpmw.Auth resolves the acting Reader from the credential hash and stashes it in the request context; the retired API_TOKEN resolves to the owner until API_TOKEN_GRACE_UNTIL, logged per use, on both the bearer and script-download paths - userscript handler renders the bindmounted file with the resolved Reader's credential substituted for __API_TOKEN__; a legacy-path request during grace serves the derived credential, so devices self-migrate on their next update poll - web UI: Userscripts panel with session-gated install endpoints that render the script directly (credential never in markup, address bar or a redirect) and confirm-gated rotation; atomic epoch bump + hash rewrite in the store - both userscripts carry __API_TOKEN__ placeholders; the committed global-token literal is removed (rotating at deploy retires it for real — it survives in git history) - env: TOKEN_KEY required, API_TOKEN/API_TOKEN_GRACE_UNTIL retire the legacy credential; docs and compose updated
This commit is contained in:
+13
-2
@@ -1,8 +1,19 @@
|
||||
# Copy to .env and fill in. Never commit the real .env.
|
||||
|
||||
# Long random secret shared with the userscript's API_TOKEN. Generate one:
|
||||
# Secret every Reader's userscript credential is derived from (issue #24):
|
||||
# the backend rebuilds install URLs from it, and only SHA-256 hashes of the
|
||||
# credentials ever touch the database. Generate one:
|
||||
# openssl rand -hex 32
|
||||
API_TOKEN=changeme-generate-a-long-random-token
|
||||
TOKEN_KEY=changeme-generate-a-long-random-token
|
||||
|
||||
# Retired global credential, kept only during the cutover window so
|
||||
# already-installed scripts keep working. Remove both it and
|
||||
# API_TOKEN_GRACE_UNTIL once the window has passed and every device has
|
||||
# reinstalled through the web UI.
|
||||
# API_TOKEN=
|
||||
# Moment the retired credential stops resolving to the owner (YYYY-MM-DD or
|
||||
# RFC3339). Enforced in code on every request; unset means it is already dead.
|
||||
# API_TOKEN_GRACE_UNTIL=2026-08-22
|
||||
|
||||
# The owner's Discord user ID — the one Reader every bookmark belongs to
|
||||
# (seeded at startup). Discord snowflake, e.g. 1046923170000000000.
|
||||
|
||||
Reference in New Issue
Block a user