Skip to content

fix: harden SEP-10 challenge and JWT token validation - #133

Merged
Cjay-Cyber-2 merged 3 commits into
mergepay:mainfrom
AdaBebe0:fix/sep10-hardening
Jul 31, 2026
Merged

fix: harden SEP-10 challenge and JWT token validation#133
Cjay-Cyber-2 merged 3 commits into
mergepay:mainfrom
AdaBebe0:fix/sep10-hardening

Conversation

@AdaBebe0

Copy link
Copy Markdown
Contributor

Summary

Closes #111

SEP-10 authentication already delegated most structural validation (server signing key, home domain, web_auth_domain, network passphrase, basic timebounds) to the Stellar SDK's WebAuth.readChallengeTx/verifyChallengeTxThreshold/verifyChallengeTxSigners helpers. This PR closes the remaining gaps called out in the issue:

  • Challenge replay: a signed challenge could previously be submitted to /auth/verify more than once within its validity window, minting a fresh token each time. verifyChallenge now records the consumed challenge transaction's hash and rejects a second use of the same signed transaction. The guard is in-memory and pruned by expiry; a multi-instance deployment would need a shared store (e.g. Redis) for the same guarantee across nodes — noted inline in src/services/sep10.ts.
  • Detail leakage on auth failure: challenge validation previously echoed the underlying SDK error message back to the client (e.g. invalid_challenge: <sdk internal message>). All challenge failures — malformed XDR, wrong signer, wrong domain, wrong network, expired timebounds, replay — now resolve to a single generic 401 UNAUTHORIZED response with no validation detail attached, and nothing is logged.
  • JWT claim enforcement: signToken/verifyToken now set and enforce alg (pinned to HS256), iss, and aud explicitly, backed by new JWT_ISSUER/JWT_AUDIENCE config values (defaulting to the API's own host, like the existing SEP-10 domain config). A token minted for a different environment/audience, with a different algorithm, or with a malformed claim shape is rejected before its sub/pk are ever trusted.
  • Fail-fast signing key config: previously, an unset SEP10_SIGNING_SECRET silently fell back to a random per-process keypair. In production this would mean challenges minted by one instance (or before a restart) can never be verified by another. The server now refuses to start in production without a configured signing key (dev/test behavior is unchanged).

No route authorization behavior changed beyond correcting these authentication gaps — requireMembership/requireAdmin and route-level checks are untouched.

Testing

  • tests/sep10-hardening.test.ts (new): expired challenge, wrong network, wrong home domain, wrong signer, malformed XDR, challenge replay, and that rejection messages don't leak SDK internals.
  • tests/plugins/auth.test.ts (new): issuer/audience embedding, rejection on wrong secret/issuer/audience/algorithm/expiration, missing claims, and that a tampered payload (swapped account) fails signature verification.
  • Existing tests/sep10.test.ts and full route test suite remain green.
npm test              # 92 passed
npm run build          # clean
npm run lint            # 1 pre-existing failure in src/services/money.ts (prefer-const), unrelated to this change and present on main before this branch

npm run test:integration requires a live server + database/testnet connection that isn't available in this sandbox, so it wasn't run end-to-end here; it depends only on src/routes/auth.ts and src/services/sep10.ts, which keep the same request/response contracts.

Reject SEP-10 challenge replay by tracking consumed challenge
transaction hashes for the duration of their validity window, and
stop leaking underlying SDK/validation error details in auth failure
responses so all challenge failures resolve to a single generic
unauthorized response.

Enforce JWT algorithm, issuer, and audience on both sign and verify so
a token minted for a different environment or signed with a different
algorithm is rejected outright, and validate the decoded claim shape
before trusting it. Fail fast at startup if SEP10_SIGNING_SECRET is
missing in production, since the previous silent per-process fallback
key breaks verification across restarts and multi-instance
deployments.

Closes mergepay#111
@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@AdaBebe0 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Cjay-Cyber-2
Cjay-Cyber-2 merged commit 95b057d into mergepay:main Jul 31, 2026
1 check failed
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.

Harden SEP-10 challenge and token validation

2 participants