Skip to content

OAUTH-001A1H — Refuse stale Strava refresh persistence #606

Closed

Description

@daliu

Parent: #88

Officer impact: After a future protected Functions release, a slow Strava refresh response cannot persist after the stored access-token, refresh-token, or expiry tuple changes, disappears, or becomes malformed. There is no current visible page change and no officer action.
Officer documentation: None — this is a server-only secret-persistence boundary. Existing SOURCE ONLY / NOT LIVE Strava failure guidance remains accurate.
Deployment evidence: Source, tests, merge, website, runmprc.com, Firebase, Strava provider, production data, and live behavior are all unverified at issue creation.

Problem

The trusted refresh path reads an expired access/refresh/expiry tuple, calls Strava, and then performs an unconditional merge write. A concurrent refresh, reconnect, disconnect, or other trusted rotation can change or remove the secret while that provider request is pending. The older response may then overwrite or resurrect stale credentials. Strava requires applications to persist and use the most recently returned refresh token because an older rotated token is invalidated immediately.

Invariant and allowed transitions

After a provider response, persist it only when a fresh Firestore transaction reread exists, is structurally valid, and its exact access token, refresh token, and expiry tuple still equals the pre-provider snapshot.

  • Unchanged valid tuple: make one transactional merge write and preserve current downstream behavior.
  • Changed, missing, or malformed tuple: make no write, return the fixed private refresh failure, and make no downstream bearer call.
  • Transaction conflict retry: reread and recheck; refuse if the tuple changed.
  • Reference, read, set, commit, or post-commit acknowledgement failure: preserve the existing fixed redacted refresh failure.
  • Never log or expose raw tokens or provider response details.

Atomic scope

  • functions/strava.js: equality and compare-before-write transaction only in refresh persistence.
  • functions/strava.test.js: a trustworthy named RED case; transaction-set mock support; positive, negative, conflict-retry, and persistence-failure coverage; mechanical updates to existing refresh persistence assertions required by direct-set-to-transaction movement.
  • SECURITY.md: one narrow current-truth row/hunk for OAUTH-001A1H and RISK-024.

Acceptance

  • Record a focused test-only RED result against the exact base.
  • Prove stale and deleted tuples are preserved/refused with no downstream request.
  • Prove retry after a Firestore conflict refuses changed state.
  • Prove an unchanged tuple persists once and continues normally.
  • Preserve fixed failure behavior for malformed state and transaction/write/ack failures.
  • Run focused and full relevant checks and complete independent review.
  • Record exact merge and deployment-surface evidence before closure.

Migration impact: None. The existing exact token tuple is the compare-and-set fence.

Exclusions / stop conditions

Do not expand into provider-call serialization or leasing, identical-tuple replacement detection, lost-ack reconciliation beyond current fixed behavior, response-body byte limits, client disconnect behavior, provider configuration or calls, Firebase deployment, real tokens or production data, or live verification. Do not close #88 from this child alone.

Issue creation is not a claim.

Activity

  1. added
    bugSomething isn't working
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    area:authAuthentication and authorization
    area:firebaseFirebase services and data
    size:SSmall focused issue
    type:testingTest infrastructure and quality gates
    area:privacyPersonal data, consent, minimization, retention, and privacy operations
    status:in-progressClaimed and actively being implemented
    on Aug 2, 2026
  2. self-assigned this
    on Aug 2, 2026
  3. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    CLAIMED by /root at 2026-08-02T05:59:37Z (GitHub createdAt).

    Exact base: a62c147
    Branch: codex/issue-606-strava-refresh-cas
    Parent: #88

    Owned outcome:

    • Compare the stored Strava access/refresh/expiry tuple again in a Firestore transaction before persisting a refresh response.
    • Refuse changed, missing, or malformed state without a stale write or downstream bearer request.
    • Preserve fixed, redacted failures and prove conflict-retry behavior.

    Owned paths/hunks:

    • functions/strava.js: refresh compare-and-set persistence hunk only.
    • functions/strava.test.js: OAUTH-001A1H tests, transaction-set mock support, and mechanical refresh-persistence assertion updates.
    • SECURITY.md: one separately named OAUTH-001A1H / RISK-024 current-truth hunk.

    Excluded: provider-call serialization/leases, identical-tuple replacement detection, response byte limits, client disconnect behavior, OAuth email verification, provider settings/calls, Firebase deploy, real tokens/data, live behavior, and closure of #88.

    Officer impact: After a future protected Functions release, a slow Strava refresh response cannot persist after the stored access-token, refresh-token, or expiry tuple changes, disappears, or becomes malformed. No page or officer procedure changes.
    Officer documentation: None — server-only secret persistence boundary; existing SOURCE ONLY / NOT LIVE guidance remains accurate.
    Deployment evidence: No source change, test result, merge, website publish, runmprc.com verification, Firebase deploy, provider configuration, production-data action, or live behavior is claimed by this marker.

  4. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    RED evidence on exact base a62c147:

    • Added only the named OAUTH-001A1H stale-rotation test.
    • Test-only diff SHA-256: 35821aa35db663cca52871ad6476c41e33055d7b73f9d285ac572138dd40ae61
    • Command: Node 20 Functions Jest, runInBand, filtered to refuses an older refresh response after the stored token tuple rotates.
    • Result: 1 failed, 7,566 skipped, 7,567 total. The failure was Expected Strava request to fail because the old runtime accepted the older provider response and completed downstream work.
    • git diff --check passed.
    • No runtime, documentation, provider, Firebase, production-data, or live mutation was made for this proof.
  5. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    Implementation candidate is ready in PR #607.

    Exact reviewed head: 49e81b5
    Exact base: a62c147
    Tree: 6b3a0db31e87ca7e247c92835f4ccd1a575e0bbf
    Patch SHA-256: bc2c962c56329bef7aa5664542c71b34c05d19a03f439c769910a25c26dcfef5
    PR: #607

    Outcome: a refresh response is merge-written only when a Firestore transaction rereads a valid access-token, refresh-token, and expiry tuple exactly equal to the pre-provider snapshot. Changed, deleted, malformed, and conflict-retried-to-changed state fails with the fixed private refresh result before downstream bearer calls.

    Trustworthy RED: 1 failed / 7,567 total on the exact base; test-only diff SHA-256 35821aa35db663cca52871ad6476c41e33055d7b73f9d285ac572138dd40ae61.

    Final local Node 20 evidence: A1H 8/8; Strava 711/711; full Functions 7,514 pass plus 64 intended skips; frontend 951/951; Rules 418/418; commerce emulator 63/63; SPA 11/11; static workflow/safety 93/93; Functions and frontend lint, TypeScript, diagnostic production build, and diff checks passed. Three independent reviews are GO after one documentation precision correction.

    Officer impact: After a future protected Functions release, a slow refresh response cannot persist after the stored three-field tuple changes, disappears, or becomes malformed.
    Officer documentation: None — server-only persistence boundary; existing SOURCE ONLY / NOT LIVE failure guidance remains accurate.
    Deployment evidence: Source/tests and pushed PR only. Code is not merged. The website and runmprc.com are not published or verified. Firebase is not deployed. Strava is not configured or contacted. No production data or live behavior is verified.

  6. daliu commented on Aug 2, 2026

    @daliu
    ContributorAuthor

    COMPLETE / CLOSED / CLAIM RELEASED.

    Released claim: /root at exact merged main 24ec0e5.
    Reviewed head: 49e81b5
    Base: a62c147
    Tree: 6b3a0db31e87ca7e247c92835f4ccd1a575e0bbf
    Patch SHA-256: bc2c962c56329bef7aa5664542c71b34c05d19a03f439c769910a25c26dcfef5
    PR: #607
    PR CI: 30735558743, all five jobs passed.
    Exact-main CI: 30735667378, all five jobs passed.
    Remote topic branch: deleted.

    Outcome: the source now transactionally rereads and validates the exact stored access-token, refresh-token, and expiry tuple before persisting a Strava refresh response. Changed, deleted, malformed, or conflict-retried-to-changed state stops with the fixed private refresh result and makes no downstream bearer call.

    Officer impact: After a future protected Functions release, a slow refresh response cannot persist after the stored three-field tuple changes, disappears, or becomes malformed. No page or officer action changed.
    Officer documentation: None — server-only secret persistence boundary; existing SOURCE ONLY / NOT LIVE guidance remains accurate.

    Deployment evidence by surface:

    • Source changed: yes, merged at exact main above.
    • Tests passed: yes, local Node 20 matrix plus PR and post-main hosted CI.
    • Code merged: yes.
    • Website published: no. Preview deploy 6a6ee058f96754000863d48c was ready, exact-head, HTTP 200, noindex, and unpublished. Exact-main production attempt 6a6ee128588b0c0008dec7b8 ended error with published_at null.
    • runmprc.com verified: it still reports WEB-002A — Publish bounded Shop and Events permissions containment #473 containment control 9ad6837 with source 39ab864 and tree d76b496cdc5e79015bc048cb961758abbe88b9ce, not this change.
    • Firebase deployed: no.
    • Outside provider configured or contacted: no.
    • Production data changed or inspected: no.
    • Production behavior verified: no.
    • GitHub deployments for exact main: [].

    Residuals remain under #88: provider refresh calls are not serialized; identical-tuple replacement is not detected; provider/local and post-commit acknowledgement reconciliation, IAM, encryption, audit, protected deployment, and live proof remain open. This child does not close #88.

  7. removed their assignment
    on Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:authAuthentication and authorizationarea:firebaseFirebase services and dataarea:privacyPersonal data, consent, minimization, retention, and privacy operationsarea:stravaStrava integrationbugSomething isn't workingpriority:P0Launch blocker or urgent security risksize:SSmall focused issuetype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions