Skip to content

feat(auth): per-request backend JWT + apiFactory wiring (#865)#925

Open
piyalbasu wants to merge 11 commits into
feat/864-derive-auth-keypairfrom
feat/865-per-request-jwt
Open

feat(auth): per-request backend JWT + apiFactory wiring (#865)#925
piyalbasu wants to merge 11 commits into
feat/864-derive-auth-keypairfrom
feat/865-per-request-jwt

Conversation

@piyalbasu

Copy link
Copy Markdown
Contributor

TL;DR

Closes #865. Builds on #864: attaches a short-lived, per-request auth token to every Freighter Mobile call to freighter-backend-v2, so the backend can attribute requests to a user. When the wallet is unlocked the request carries the token; when locked it goes out anonymously (the backend is permissive today, so nothing breaks). If a token is rejected as expired, the request is retried once with a fresh one. This mirrors the extension's per-request JWT work and produces the same token contract (only the issuer differs).

Stacked on #920 (#864) — review/merge that first. This PR's base is the feat/864 branch.

Implementation details (for agents)

What changed:

  • src/services/auth/buildAuthJwt.ts (new) — pure, synchronous EdDSA (alg:"EdDSA") compact JWS. Claims: sub=hex pubkey, iss="freighter-mobile", iat, exp=iat+15, bodyHash=hex SHA-256 of the raw body string (empty-input hash for GET), methodAndPath="METHOD /full/path?query". base64url, unpadded. Parity port of the extension's builder; only iss differs.
  • src/services/auth/attachAuth.ts (new) — attachAuthInterceptors(instance). Request interceptor: if getAuthKeypair() (from [Mobile] Derive auth keypair from seed for backend authentication #864) returns a keypair, derive the full server path from baseURL + url (incl. /api/v1 + query, matching the server's r.URL.RequestURI()), build the JWT over (method, path, string body), set Authorization: Bearer …; locked → no header. Response interceptor: on 401, retry exactly once with a freshly built token (guarded by __isAuthRetry), then reject.
  • src/services/apiFactory.ts — new optional configureInstance hook, invoked on the raw axios instance before the error-normalizer interceptor is registered. This is load-bearing: axios's normalizer converts errors to a plain ApiError (dropping config/response), so the auth interceptors must register first to see a real 401 and retry. Optional + no-op for other consumers (freighterBackendV1 unaffected).
  • src/services/backend.tsfreighterBackendV2 created with configureInstance: attachAuthInterceptors. Migrated the two V2 POST call sites (fetchTokenPrices, fetchCollectibles) to the mandated contract: pre-serialized string body + query in the URL (not axios params) + explicit Content-Type. Without this, the interceptor (which runs before axios serializes) would hash an empty body and omit the query → a signed-but-wrong token that silently 401s once the backend requires auth.
  • __tests__/services/auth/authJwt.integration.test.ts (new) — gated E2E vs staging whoami (skipped unless BACKEND_V2_URL/IS_INTEGRATION_MODE set): valid→200+userId, tampered-body→401, flipped-sig→401, expired→401.

Verification: full suite 2593 passing / 6 skipped (186 suites); tsc --noEmit clean. attachAuth has 13 tests including 3 that drive a real 401 through the full apiFactory chain (proving the retry actually fires and re-signs with a fresh iat). Backend call-site tests strengthened to lock the string-body/query-in-url contract. Reviewed task-by-task plus a whole-branch review that caught the retry-ordering bug and the call-site contract gap (both fixed here).

Follow-ups / out of scope: Contacts UI/CRUD (separate ticket); the backend permissive→required flip (freighter-backend-v2#114), gated on extension + mobile adoption. The two migrated call sites are the only V2 POSTs today; a future audit should confirm any new v2 caller also passes a string body + query-in-url.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-4d0ae7733dc155d1c256 (SDF collaborators only — install instructions in the release description)

@piyalbasu

Copy link
Copy Markdown
Contributor Author

Extension parity: this ports two extension PRs:

@piyalbasu piyalbasu force-pushed the feat/865-per-request-jwt branch from f5cb136 to e169e8e Compare July 8, 2026 17:15
@piyalbasu piyalbasu marked this pull request as ready for review July 8, 2026 18:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e169e8e05d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/services/auth/attachAuth.ts
@piyalbasu piyalbasu force-pushed the feat/864-derive-auth-keypair branch from 342d60a to 9f4ec75 Compare July 8, 2026 19:20
@piyalbasu piyalbasu force-pushed the feat/865-per-request-jwt branch from e169e8e to f911796 Compare July 8, 2026 19:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f911796684

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/services/auth/attachAuth.ts
@piyalbasu piyalbasu force-pushed the feat/865-per-request-jwt branch from 80f48f7 to 814c2a4 Compare July 8, 2026 20:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0e1e3fbdd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/services/auth/attachAuth.ts Outdated
piyalbasu and others added 9 commits July 8, 2026 16:25
…ckendV2 (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…401 retry fires (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… retry for anonymous 401s (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…path contract doc (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@piyalbasu piyalbasu force-pushed the feat/865-per-request-jwt branch from b0e1e3f to 2cd79e6 Compare July 8, 2026 20:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c5e5851dc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const entries = Object.entries(params as Record<string, unknown>);
if (entries.length === 0) return url;
qs = new URLSearchParams(
entries.map(([k, v]) => [k, String(v)] as [string, string]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip nullish params before folding

When an unlocked request supplies optional Axios params, this conversion serializes null and undefined as literal strings before deleting config.params; Axios normally omits those values. For a call such as freighterBackendV2.get('/items', { params: { cursor: undefined } }), the authenticated path now sends and signs ?cursor=undefined, while the locked path still omits it, so backend filters/pagination can change depending on auth state. Filter nullish entries before building the merged query.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant