diff --git a/doc/endorser-jwt-background-prefetch-options.md b/doc/endorser-jwt-background-prefetch-options.md new file mode 100644 index 00000000..4023a6d2 --- /dev/null +++ b/doc/endorser-jwt-background-prefetch-options.md @@ -0,0 +1,152 @@ +# Options: expired JWT during background “New Activity” prefetch (mobile) + +**Date:** 2026-03-26 17:29 PST +**Audience:** TimeSafari / crowd-funder team; **Endorser server** maintainers (auth + API policy) +**Context:** Android Capacitor app, `POST /api/v2/report/plansLastUpdatedBetween`, native `TimeSafariNativeFetcher` invoked from WorkManager at **T−5 minutes** before the daily notification. + +--- + +## Problem (short) + +New Activity notifications prefetch Endorser data in **background** (no JavaScript, no WebView). The HTTP client uses a **Bearer JWT** supplied earlier via `configureNativeFetcher` / `getHeaders(activeDid)`. + +If the **access token’s `exp`** is **before** prefetch time, the API returns **400** with a body like: + +```json +{ + "error": { + "message": "JWT failed verification: ... JWT has expired: exp: … < now: …", + "code": "JWT_VERIFY_FAILED" + } +} +``` + +We **cannot** rely on the user opening the app immediately before prefetch (T−5), so **client-only** mitigations (e.g. refresh JWT on app resume) **reduce** failures but **do not guarantee** a valid token for headless background work. + +--- + +## Why this is different from normal in-app API calls + +| In-app | Background prefetch | +|--------|----------------------| +| `getHeaders()` runs in JS when needed; user often recently active | WorkManager runs **without** Capacitor / passkey / session refresh | +| Short TTL tokens are refreshed as the user uses the app | Same token may sit in native memory until **T−5** (or longer) | + +So **server-side** and **architecture** choices matter for this feature. + +--- + +## Options (for decision) + +### 1. Increase access token TTL (Endorser / IdP) + +**Idea:** Issue access JWTs with a longer `exp` so that **configure time → prefetch time** (often **5+ minutes**, sometimes **24h+** if the user rarely opens the app) usually still falls inside validity. + +| Pros | Cons | +|------|------| +| Simple to explain; one policy change | Longer-lived bearer tokens increase risk if exfiltrated; mitigate with scope, rotation, monitoring | +| No client protocol change | May not fit strict security posture without a dedicated scope | + +**Endorser owner:** token lifetime, scopes, and whether a **dedicated** lifetime or scope for “mobile background read” is acceptable. + +--- + +### 2. Scoped long-lived token for report reads only (Endorser) + +**Idea:** Mint a **separate** access token (or sub-scope) valid only for **read-only report** endpoints (`plansLastUpdatedBetween`, etc.), with a **longer TTL** than the interactive session token. + +| Pros | Cons | +|------|------| +| Limits blast radius vs “longer JWT for everything” | Requires auth model + issuance path; client must store/use this token only for prefetch | + +**Endorser owner:** feasibility of **narrow scope** + **longer TTL** for this use case. + +--- + +### 3. Refresh token or device grant (Endorser + mobile native) + +**Idea:** Client stores a **refresh token** (or OAuth **device** grant) in **Android Keystore / iOS Keychain**. Before `plansLastUpdatedBetween`, **native** code (no JS) exchanges it for a **new access token**. + +| Pros | Cons | +|------|------| +| Standard pattern; short TTL for access tokens remains | Endorser must support refresh (or equivalent); secure storage + rotation; **both** client and server work | +| Works when app is backgrounded for days | Implementation cost on mobile | + +**Endorser owner:** refresh endpoint, token rotation, revocation. +**Mobile owner:** native fetch path, secure storage, failure handling. + +--- + +### 4. Backend proxy / BFF (TimeSafari backend + Endorser) + +**Idea:** Phone calls **your** backend with a **device session** (or FCM registration id); **server** uses **server-to-server** credentials or a **service account** to call Endorser. The device **never** sends an Endorser JWT for this path. + +| Pros | Cons | +|------|------| +| No Endorser JWT lifetime problem on device | New service, auth, rate limits, privacy review | +| Central place for logging, abuse control | Operational cost | + +**Endorser owner:** partner / S2S auth model for the BFF. +**Product team:** hosting and trust boundaries. + +--- + +### 5. “Cron” or periodic jobs on the device to refresh JWT (JS) + +**Idea:** Use something like a **cron** schedule to refresh tokens. + +**Reality:** Scheduled **native** jobs can run, but **Capacitor / `getHeaders()` / passkey** do **not** run reliably in that context without waking the **WebView**. So **“cron”** only helps if refresh is **fully native** (see option 3) or you accept **unreliable** wake + JS. + +**Not recommended** as the primary fix unless paired with **native refresh** or **server** changes. + +--- + +### 6. Product / UX constraints (no server change) + +**Idea:** Accept that **headless** API calls may fail if the session is stale; show **fallback** copy; or require “open app once per day” for best results. + +| Pros | Cons | +|------|------| +| No Endorser change | Does not meet “API-driven notification” expectation for inactive users | + +--- + +## Client-side mitigations already in play (not sufficient alone) + +- **`configureNativeFetcherIfReady()`** after startup and when **Account** / identity is ready. +- **`appStateChange` → `isActive`:** refresh native fetcher when the app returns to foreground (reduces staleness when the user **does** open the app). +- **Error logging** of 400 bodies for diagnosis. + +These **do not** guarantee a fresh JWT at **T−5** if the user never opens the app before prefetch. + +--- + +## Suggested decision order + +1. **Align on security posture:** Is a **longer TTL** or **scoped long-lived read token** acceptable for Endorser? +2. If not, is **refresh token in native** (option 3) or **BFF** (option 4) on the roadmap? +3. **Parallel:** UX fallback when API is unavailable (option 6) so the app never silently looks “broken.” + +--- + +## References (this repo) + +| Topic | Location | +|--------|----------| +| Native fetcher + JWT from `getHeaders` | `src/services/notifications/nativeFetcherConfig.ts` | +| Android POST + errors | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` | +| Web `plansLastUpdatedBetween` + `afterId` | `src/libs/endorserServer.ts` (`getStarredProjectsWithChanges`) | +| New Activity / dual schedule | `doc/notification-from-api-call.md`, `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md` | + +--- + +## Open questions for Endorser (server developer) + +1. What is the **current access token TTL** and can it be **increased** for mobile clients, or **per-scope**? +2. Is **refresh token** (or similar) available for **non-interactive** renewal? +3. Would a **read-only** scope for `plansLastUpdatedBetween` with a **longer** lifetime be acceptable? +4. Is there an existing **server-to-server** or **partner** path that a **BFF** could use instead of user JWT on device? + +--- + +*This document is for internal planning and decision; update it when the team chooses an approach.*