docs: add Endorser JWT options for background New Activity prefetch
Document expired-token causes, client limits, and server-side options (TTL, scoped tokens, refresh, BFF) plus questions for Endorser maintainers.
This commit is contained in:
152
doc/endorser-jwt-background-prefetch-options.md
Normal file
152
doc/endorser-jwt-background-prefetch-options.md
Normal file
@@ -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.*
|
||||
Reference in New Issue
Block a user