Skip to content

fix(auth): implement real SEP-10 Ed25519 signature verification#1

Open
Iceeyyou2 wants to merge 1 commit into
mainfrom
fix/sep10-signature-verification
Open

fix(auth): implement real SEP-10 Ed25519 signature verification#1
Iceeyyou2 wants to merge 1 commit into
mainfrom
fix/sep10-signature-verification

Conversation

@Iceeyyou2

Copy link
Copy Markdown
Owner

Summary

Fixes Issue #1 — the /api/auth/verify route was issuing tokens for any public key without performing any cryptographic validation, allowing full account impersonation.


Changes

src/lib/auth/challenge-store.ts (new)

Server-side in-process challenge registry with:

  • 5-minute TTL — challenges expire and are rejected after the window closes
  • Replay protection — each challenge carries a used flag; a second consumeChallenge() call returns already_used
  • _clearStore() helper for deterministic test teardown

src/lib/auth/jwt.ts (new)

Minimal standards-compliant HS256 JWT implementation on top of Node's crypto module (no external JWT library required):

  • signJwt(publicKey) — produces a 24-hour token with sub, iat, exp
  • verifyJwt(token) — validates signature with timing-safe comparison, checks expiry, returns typed result
  • Reads secret from JWT_SECRET env var (enforced in production; dev fallback supplied)

src/app/api/auth/challenge/route.ts

  • Validates public key is a well-formed Ed25519 Stellar address via StrKey.isValidEd25519PublicKey()
  • Generates a crypto.randomBytes(32) nonce-based challenge string
  • Stores challenge server-side via storeChallenge() before returning it to the client
  • Removed the unused token field from the response

src/app/api/auth/verify/route.ts

Before: issued a Base64 token from raw input with no verification.
After:

  1. Validates public key format
  2. Calls consumeChallenge() — rejects expired or already-used challenges
  3. Decodes the base64 signedChallenge and calls Keypair.verify(challengeBuffer, signatureBuffer)
  4. Issues a signed HS256 JWT only if all three checks pass

src/app/api/auth/me/route.ts

Before: returned hardcoded data as long as any Authorization header was present.
After: calls verifyJwt() — rejects malformed, tampered, or expired tokens with the appropriate 401 message; extracts sub (public key) from the verified payload.


Tests (21 passing)

File Tests
challenge-store.test.ts 5 — store/consume happy path, replay, TTL expiry, near-expiry
jwt.test.ts 6 — sign/verify, tampered payload, tampered sig, malformed, expired, empty
sep10-auth.integration.test.ts 10 — valid full flow, JWT usable by /me, wrong keypair, bogus sig, no challenge, replay, TTL, tampered subject, expired JWT
Test Files  3 passed (3)
     Tests  21 passed (21)

Acceptance Criteria

Criterion Status
verify validates Ed25519 signature against stored challenge
Challenges expire after configurable TTL (default 5 min)
Replayed challenges are rejected
auth/me validates JWT signature, not just header presence
Integration tests cover valid and invalid signature cases

Security Notes

  • JWT_SECRET must be set to a ≥ 32-character random value in production; the server throws at startup if it's missing or too short
  • The in-process challenge store is suitable for single-instance deployments; a Redis-backed store is recommended for multi-instance production setups

Closes #1

- challenge/route.ts: validate public key format (StrKey), generate
  nonce-based challenge, store server-side with 5-min TTL
- verify/route.ts: consume stored challenge (TTL + replay protection),
  verify Ed25519 sig via Keypair.verify(), issue signed HS256 JWT
- me/route.ts: validate JWT signature + expiry via verifyJwt() instead
  of only checking header presence
- src/lib/auth/challenge-store.ts: in-process Map store with TTL and
  used-flag replay protection; _clearStore() for tests
- src/lib/auth/jwt.ts: HS256 JWT sign/verify using Node crypto HMAC-SHA256
  with timing-safe comparison; 24h token lifetime
- 21 passing tests: challenge-store, jwt unit tests, SEP-10 integration
  tests covering valid sig, wrong keypair, bogus sig, replay, TTL expiry,
  tampered JWT, and expired JWT
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