Skip to content

feat(auth): add POST /auth/challenge endpoint - #33

Merged
codebestia merged 6 commits into
ShadeProtocol:mainfrom
josephchimebuka:feat/auth-challenge-endpoint
Jul 30, 2026
Merged

feat(auth): add POST /auth/challenge endpoint#33
codebestia merged 6 commits into
ShadeProtocol:mainfrom
josephchimebuka:feat/auth-challenge-endpoint

Conversation

@josephchimebuka

@josephchimebuka josephchimebuka commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds POST /api/v1/auth/challenge that validates a Stellar public key with StrKey.isValidEd25519PublicKey, issues a 5-minute single-use nonce, and returns { message, nonce, expiresAt }.
  • Hardens nonce creation: cryptographically random 32-byte hex nonce, expired AuthNonce cleanup before each challenge, and the existing deterministic buildChallengeMessage format so /auth/verify stays compatible.
  • Adds unit and integration coverage for valid/invalid addresses and cleanup behavior.

Test plan

  • npm test -- --testPathPatterns='auth\.(services|routes)' (26 passing)
  • POST /api/v1/auth/challenge with a valid G... address returns 200 { message, nonce, expiresAt }
  • Invalid / missing address returns 400 { error: "Invalid Stellar address" }
  • Expired nonces are deleted before a new challenge is persisted
  • Challenge message matches what /auth/verify reconstructs for signature checks

Closes #6

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added a POST /challenge endpoint that validates Stellar Ed25519 public addresses and returns a nonce challenge (message, nonce, expiration).
  • Bug Fixes
    • Improved nonce issuance with atomic cleanup and concurrency-safe creation to ensure only one active unused nonce per address.
    • Updated signature verification to use the stored challenge message for verification.
  • Tests
    • Added/expanded unit, integration, and E2E coverage for challenge creation, invalid/missing address handling, concurrency behavior, and the full sign→verify flow.
  • Chores
    • Added database indexes to enforce “one active nonce per address” behavior.

Issue a time-limited, single-use challenge tied to a validated Stellar
address, clean up expired AuthNonce rows, and keep the deterministic
message format used by /auth/verify.

Closes ShadeProtocol#6

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds POST /challenge for validated Stellar addresses, generates and persists hexadecimal challenges transactionally, enforces one active nonce per address, verifies stored challenge messages, and adds integration, unit, concurrency, and end-to-end coverage.

Changes

Stellar authentication challenge flow

Layer / File(s) Summary
Transactional nonce lifecycle
src/services/auth.services.ts, prisma/schema.prisma, prisma/migrations/..., tests/unit/auth.services.test.ts, tests/integration/auth.challenge.concurrency.test.ts
createNonce cleans stale records, creates challenges transactionally, handles unique-constraint races, and enforces one active nonce per address.
Challenge validation and routing
src/controllers/auth.controllers.ts, src/routes/auth.routes.ts, tests/integration/auth.routes.test.ts
The new controller validates Stellar Ed25519 addresses and returns challenge or error responses through POST /challenge.
Stored challenge verification
src/services/auth.services.ts, tests/unit/auth.services.test.ts, tests/unit/auth.sign-verify.e2e.test.ts
Signature verification uses the stored challenge message, with wallet verification and authentication token coverage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant auth.routes
  participant createChallengeController
  participant createNonce
  participant AuthNonce
  Client->>auth.routes: POST /challenge with address
  auth.routes->>createChallengeController: forward request
  createChallengeController->>createNonce: validated address
  createNonce->>AuthNonce: delete stale nonces and create challenge
  AuthNonce-->>createNonce: message, nonce, expiresAt
  createNonce-->>createChallengeController: challenge result
  createChallengeController-->>Client: HTTP 200 response
Loading

Suggested reviewers: g-elm

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the POST /auth/challenge endpoint.
Linked Issues check ✅ Passed The PR implements the requested challenge route, address validation, nonce persistence, cleanup, and deterministic message/response shape.
Out of Scope Changes check ✅ Passed The schema, migration, and added tests all support the auth challenge flow, with no clearly unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/controllers/auth.controllers.ts`:
- Line 32: Remove the unused error binding from the catch clause in the relevant
auth controller method, while preserving the existing catch block behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d427b60f-fa66-4009-9d90-8d66a28eb7bc

📥 Commits

Reviewing files that changed from the base of the PR and between c197dff and a2fd584.

📒 Files selected for processing (5)
  • src/controllers/auth.controllers.ts
  • src/routes/auth.routes.ts
  • src/services/auth.services.ts
  • tests/integration/auth.routes.test.ts
  • tests/unit/auth.services.test.ts

Comment thread src/controllers/auth.controllers.ts
@codebestia

Copy link
Copy Markdown
Contributor

GM @josephchimebuka
Please address the coderabbit review

Co-authored-by: Cursor <cursoragent@cursor.com>

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello @josephchimebuka
Good job so far.
From my personal review, here are some changes i recommend to be added.

  • Add error logging in createChallengeController's catch block and/or propagate the error to your app logger (include request context except secrets). This helps diagnose failures in staging/production. For now, use a mock or console.error till a better app logger infrastructure is provided.
  • Confirm & enforce a DB unique constraint on active nonces for an address and implement createNonce to either upsert or use a serialized transaction to avoid race conditions when two requests run in parallel.
  • Fix verification to use the stored message (authNonce.message) or otherwise guarantee createdAt parity. (Critical)
  • Wrap deleteMany + create in a transaction to prevent duplicate active nonces or unexpected behavior. (High)
  • Update/Add tests for concurrency and full sign/verify E2E flow

codebestia and others added 2 commits July 29, 2026 02:26
Wrap nonce cleanup and creation in a transaction, enforce one active
nonce per address via a partial unique index, verify signatures against
the stored challenge message, add error logging in the challenge
controller, and expand concurrency plus sign/verify E2E tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
prisma/schema.prisma (1)

60-61: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Index the expiry cleanup predicate.

Each challenge deletes expired rows globally, but @@index([address]) cannot support expiresAt < now. Add @@index([expiresAt]) and its migration to prevent this cleanup from becoming a table scan as nonce traffic grows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/schema.prisma` around lines 60 - 61, Add an index on the challenge
model’s expiresAt field alongside the existing address index, and create the
corresponding database migration. Ensure the expiry cleanup query using
expiresAt < now can use this index rather than scanning the table.
tests/unit/auth.sign-verify.e2e.test.ts (2)

35-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate authNonceRecord mock construction.

Both tests build near-identical stored-nonce mock objects differing only in nonce/message/expiresAt. Consider a small factory to reduce duplication.

♻️ Example helper
+function buildAuthNonceRecord(overrides: Partial<Record<string, unknown>> = {}) {
+  return {
+    id: 'uuid-1',
+    address,
+    usedAt: null,
+    createdAt: mockDate,
+    merchantId: null,
+    ...overrides,
+  };
+}

Also applies to: 80-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/auth.sign-verify.e2e.test.ts` around lines 35 - 44, Introduce a
small factory helper for constructing authNonceRecord mocks and use it in both
test cases. Keep shared fields—such as id, address, usedAt, createdAt, and
merchantId—centralized in the helper while allowing nonce, message, and
expiresAt to vary per test.

31-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a negative-path signature test.

This file only exercises the happy path (valid signature against the stored message). Since it's scoped to "stored challenge verification," a tampered-signature or mismatched-address case would give confidence verifySignature actually rejects bad signatures rather than always returning valid: true.

test('tampered signature fails verification against the stored message', async () => {
  const nonce = 'c'.repeat(64);
  const message = buildChallengeMessage(address, nonce, mockDate);
  const badSignature = keypair.sign(Buffer.from(message + 'x', 'utf-8')).toString('hex');
  prismaMock.authNonce.findUnique.mockResolvedValue({
    id: 'uuid-1', address, nonce, message,
    expiresAt: new Date('2026-06-21T12:05:00.000Z'),
    usedAt: null, createdAt: mockDate, merchantId: null,
  });

  const result = await verifySignature(address, nonce, badSignature);

  expect(result.valid).toBe(false);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/auth.sign-verify.e2e.test.ts` around lines 31 - 107, Add a
negative-path test alongside the existing wallet-signed challenge test in the
verifySignature suite. Mock authNonce.findUnique with the stored challenge, sign
a tampered message or otherwise provide an invalid signature, call
verifySignature with the matching address and nonce, and assert the result is
invalid while preserving the existing happy-path test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/auth.services.ts`:
- Around line 26-35: Handle concurrent same-address challenge issuance in the
authNonce transaction by adding per-address serialization or a defined
retry/conflict policy, so concurrent requests return the intended challenge
response instead of an unhandled P2002 failure. In
tests/unit/auth.services.test.ts lines 45-49, stop modeling the transaction mock
as serialized behavior; in lines 112-130, add a database-backed concurrent test
asserting the chosen outcome.

---

Nitpick comments:
In `@prisma/schema.prisma`:
- Around line 60-61: Add an index on the challenge model’s expiresAt field
alongside the existing address index, and create the corresponding database
migration. Ensure the expiry cleanup query using expiresAt < now can use this
index rather than scanning the table.

In `@tests/unit/auth.sign-verify.e2e.test.ts`:
- Around line 35-44: Introduce a small factory helper for constructing
authNonceRecord mocks and use it in both test cases. Keep shared fields—such as
id, address, usedAt, createdAt, and merchantId—centralized in the helper while
allowing nonce, message, and expiresAt to vary per test.
- Around line 31-107: Add a negative-path test alongside the existing
wallet-signed challenge test in the verifySignature suite. Mock
authNonce.findUnique with the stored challenge, sign a tampered message or
otherwise provide an invalid signature, call verifySignature with the matching
address and nonce, and assert the result is invalid while preserving the
existing happy-path test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f8fd2942-15c7-4783-9e5c-951ff209791e

📥 Commits

Reviewing files that changed from the base of the PR and between 81658ec and cfd3401.

📒 Files selected for processing (7)
  • prisma/migrations/20260729100000_auth_nonce_active_address_unique/migration.sql
  • prisma/schema.prisma
  • src/controllers/auth.controllers.ts
  • src/services/auth.services.ts
  • tests/integration/auth.routes.test.ts
  • tests/unit/auth.services.test.ts
  • tests/unit/auth.sign-verify.e2e.test.ts

Comment thread src/services/auth.services.ts Outdated
josephchimebuka and others added 2 commits July 29, 2026 10:38
Acquire a per-address Postgres advisory lock inside the nonce
transaction, return an existing active challenge on P2002 instead of
500, and add concurrency simulation plus conflict-policy tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/auth.services.ts (1)

34-53: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Confirm concurrent same-address challenge semantics

createNonceInTransaction serializes challenge creation per address and deletes any existing active unused nonce before inserting a new one, while verifySignature rejects invalid or replaced nonces. This makes last-issued-nonce semantics explicit, but a later challenge call can render previously returned nonce(s) invalid before /auth/verify is reached. If that behavior is not intended, keep the per-address lock but preserve the earlier active nonce unless the caller explicitly requests a refresh/replay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/auth.services.ts` around lines 34 - 53, The
createNonceInTransaction flow currently deletes every active unused nonce for
the address, enforcing last-issued-nonce behavior. Confirm the intended
concurrent challenge semantics; if earlier challenges must remain valid, retain
the per-address advisory lock but remove the address-and-unused deletion
condition and only delete expired nonces, unless an explicit refresh or replay
request is provided.
🧹 Nitpick comments (1)
tests/integration/auth.challenge.concurrency.test.ts (1)

113-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the assertion to make the last-write-wins outcome explicit.

This test proves 5 parallel same-address requests each resolve successfully but only 1 nonce survives — it doesn't verify whether the 4 discarded nonces are the ones actually returned to earlier callers (i.e., that already-handed-out challenges become silently invalid). Asserting results against store (e.g., only the last-settled result matches the surviving activeNonces[0]) would make this intentional behavior explicit and guard against accidental regressions/refinements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/auth.challenge.concurrency.test.ts` around lines 113 - 129,
The concurrency test should explicitly verify the last-write-wins behavior:
after confirming one active nonce remains, assert that only the result from the
last-settled request matches activeNonces[0], while the other four returned
challenges do not match the surviving nonce. Use the existing results,
activeNonces, and createNonce flow without changing the response-shape
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/services/auth.services.ts`:
- Around line 34-53: The createNonceInTransaction flow currently deletes every
active unused nonce for the address, enforcing last-issued-nonce behavior.
Confirm the intended concurrent challenge semantics; if earlier challenges must
remain valid, retain the per-address advisory lock but remove the
address-and-unused deletion condition and only delete expired nonces, unless an
explicit refresh or replay request is provided.

---

Nitpick comments:
In `@tests/integration/auth.challenge.concurrency.test.ts`:
- Around line 113-129: The concurrency test should explicitly verify the
last-write-wins behavior: after confirming one active nonce remains, assert that
only the result from the last-settled request matches activeNonces[0], while the
other four returned challenges do not match the surviving nonce. Use the
existing results, activeNonces, and createNonce flow without changing the
response-shape assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc38500e-014b-4c00-b2dd-741f8dcdf957

📥 Commits

Reviewing files that changed from the base of the PR and between cfd3401 and c2a4eb0.

📒 Files selected for processing (5)
  • src/services/auth.services.ts
  • tests/integration/auth.challenge.concurrency.test.ts
  • tests/integration/auth.routes.test.ts
  • tests/unit/auth.services.test.ts
  • tests/unit/auth.sign-verify.e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/auth.sign-verify.e2e.test.ts
  • tests/integration/auth.routes.test.ts

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Thank you for your contribution.

@codebestia
codebestia merged commit 4d80c5f into ShadeProtocol:main Jul 30, 2026
3 checks passed
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.

Auth: Challenge Endpoint (POST /auth/challenge)

2 participants