Skip to content

Prove seller wallet ownership with a signed challenge session instead of a public key in the query string #586

Open

Description

@CemAyyildiz

Context

getInvoice in backend/src/routes/invoice.handlers.ts returns the full stored record, including customer name, email and payer identity, when ?sellerPublicKey= matches the invoice's seller. That same key is part of PublicInvoiceDto (shared/invoice.ts) and is printed on the pay page, so anyone holding a pay link can read the seller workspace. getInvoices, getStats and getPaymentEvents use the same query-string check, and createInvoice accepts any sellerPublicKey with no proof, so pay links can be minted under someone else's wallet. Cancel signs cancel:<id> (signInvoiceCancelMessage in frontend/lib/stellar.ts). That message has no domain, network, nonce or expiry, so a captured signature stays valid forever.

Goal

Seller-only endpoints take the seller from a short-lived session that the wallet earned by signing a server-issued challenge. A public key in a query string or body never grants access by itself. Freighter stays the only identity, with no account table.

Work

  • Add GET /api/auth/challenge and POST /api/auth/session, built on the SEP-10 helpers already in @stellar/stellar-sdk (WebAuth.buildChallengeTx, readChallengeTx, verifyChallengeTxSigners). Bind them to the pinned network passphrase from shared/network.ts and a configured home domain. Nonces are single-use and expire within 5 minutes
  • Issue an HMAC-signed session token carrying the seller key, network and an expiry of at most 1h, plus a key id so the secret can be rotated from env
  • Add a requireSellerSession middleware and move getInvoices, getStats, getPaymentEvents, the workspace branch of getInvoice, createInvoice and cancelInvoice onto it. A body or query sellerPublicKey that differs from the session key returns 403
  • Add Authorization to corsOptions().allowedHeaders in backend/src/config/runtime.ts, which today allows only Content-Type and Accept
  • In the frontend, the wallet session gets a token through Freighter signTransaction, keeps it in memory per account and network, drops it on account or network switch, and challenges again on 401. frontend/lib/api.ts attaches the token and mock-api.ts mirrors the contract
  • Retire the cancel:<id> blob path in favour of the session. Any transition fallback sits behind an env flag with a removal note
  • Add tests for the pay-link-to-workspace leak, replayed or expired challenges, a token from the other network, and a wallet switch mid-dashboard

Expected diff

18–25 files. A new auth route and middleware, the handlers moved onto them, and the frontend wallet session and API client attaching the token.

Areas involved

backend/src/routes/invoice.handlers.ts, backend/src/routes/invoice.routes.ts, backend/src/utils/signature-verification.ts, backend/src/config/runtime.ts, backend/src/config/stellar.ts, backend/src/server-dual.ts, backend/src/server-mvp.ts, backend/src/server.ts, backend/env.example.txt, backend/env.mvp.example, shared/network.ts, shared/invoice.ts, frontend/lib/stellar.ts, frontend/lib/wallet-session.js, frontend/lib/wallet-session.d.ts, frontend/lib/api.ts, frontend/lib/mock-api.ts, frontend/app/dashboard/page.tsx, frontend/app/invoice/[id]/page.tsx, frontend/components/PaymentEventsFeed.tsx, frontend/components/InvoiceForm.tsx, backend/tests/cross-seller-isolation.test.ts, backend/tests/wallet-scoped-invoices.test.ts, backend/tests/invoice-handlers.test.ts, frontend/tests/wallet-session.test.js, backend/src/routes/auth.routes.ts (new), backend/src/middleware/seller-session.ts (new), backend/tests/seller-session.test.ts (new).

Done when

  • GET /api/invoices/:id?sellerPublicKey=<seller key from the public DTO> without a session returns only public fields. A named test reproduces the pay-link path and asserts there is no customerEmail
  • List, stats, events, create and cancel return 401 without a session, and 403 when the body or query key differs from the session key
  • A challenge can be redeemed once. Expired, reused or other-network challenges are rejected with a typed code
  • Switching the Freighter account on the dashboard drops the old token before the next fetch
  • A CORS preflight from the configured frontend origin allows Authorization

Activity

  1. added
    enhancementNew feature or request
    help wantedExtra attention is needed
    Maybe RewardedIssue may be eligible for a GrantFox reward
    GrantFox OSSIssue tracked in GrantFox OSS
    on Oct 2, 2026
  2. Rodrigoue9 commented on Oct 3, 2026

    @Rodrigoue9

    Hi @Quittance-Labs team, I would like to work on this issue!

    Architectural Approach

    1. Challenge Generation & Nonce Management: Implement an ephemeral challenge flow (/api/auth/challenge) issuing a cryptographically secure nonce, timestamp, and strict TTL (5 minutes) held in-memory/cache.
    2. SEP-10 / ed25519 Signature Verification: Provide client-side Freighter signing utilities and backend verification confirming that the signature matches the seller's registered public key, preventing query param spoofing in getInvoice.
    3. Session Binding: Issue a short-lived bearer token or signed session header so that sensitive customer details in PublicInvoiceDto are only disclosed to the proven owner.
    4. Test Coverage: Add comprehensive integration tests verifying valid signatures, expired nonces, replayed challenge rejection, and mismatched public key rejections.

    Proof of Work

    Senior full-stack Web3 engineer specialized in Stellar/Soroban architecture (contributor to Astrea-Protocol #251, stellar-did-credit #898, and Documenso #3275).

    Could you please assign this issue to me so I can open the PR? Thanks!

  3. Ranjeet2063 commented on Oct 4, 2026

    @Ranjeet2063
    Contributor

    Hello maintainers,

    I am interested in taking up Prove seller wallet ownership with a signed challenge session instead of a public key in the query string.

    Plan:

    1. Audit storage layout and state invariants ensuring strict auth guards.
    2. Implement deterministic state transitions with safe checked arithmetic.
    3. Add hermetic unit and regression tests covering authorized, unauthorized, and boundary edge cases.
    4. Profile instruction and CPU/gas limits under Soroban host environment.

    Please assign this to me if the plan looks good. Thanks!

  4. woahwhattheheck commented on Oct 9, 2026

    @woahwhattheheck
    Contributor

    /claim #586

    I am the original author of the implementation in PR #589: #589. Please review this existing submission for acceptance and any eligible GrantFox OSS contributor reward under actual program terms; if approved, confirm allocation, payout route and timing. No particular award or settlement is claimed as received.

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

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardenhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions