Skip to content

Feature/auth middleware#13

Merged
robertocarlous merged 7 commits intoNeurowealth:mainfrom
Joaco2603:feature/auth-middleware
Mar 6, 2026
Merged

Feature/auth middleware#13
robertocarlous merged 7 commits intoNeurowealth:mainfrom
Joaco2603:feature/auth-middleware

Conversation

@Joaco2603
Copy link
Copy Markdown
Contributor

Secure Authentication System for NeuroWealth Backend

Closes #8

Summary

Implemented a full authentication system protecting all user-specific and financial endpoints.
Authentication is based on Stellar keypair ownership (challenge-response with signature verification) rather than passwords, issuing a 24-hour JWT backed by a database session on success.


What was built / changed

New files

File Purpose
src/types/express.d.ts Module augmentation — adds req.userId and req.stellarPubKey to the Express Request interface
src/utils/stellar/stellar-verification.ts StellarVerification class — owns the in-memory nonce store, purgeExpiredNonces(), verifyStellarSignature(), and resolveNetwork(). Exports a singleton and _nonceStoreForTests
src/jobs/sessionCleanup.ts connectDb()scheduleSessionCleanup() — daily cron that deletes expired sessions from the database; also runs once at startup
src/controllers/auth-controller.ts challenge, verify, logout HTTP handlers — delegates all Stellar logic to StellarVerification
src/controllers/__tests__/auth.test.ts 19 unit tests covering all auth scenarios (see Testing section)
AUTH.md Documentation — auth flow, wallet connection steps, session lifecycle, security best practices

Modified files

File Change
src/middleware/authenticate.ts Fully rewritten — validates JWT, checks live DB session, rejects expired sessions, attaches req.userId and req.stellarPubKey
src/routes/auth.ts Replaced placeholder — mounts POST /challenge, POST /verify, POST /logout (logout requires JWT)
src/config/env.ts Added config.jwt.session_ttl_hours, config.jwt.nonce_ttl_ms, config.jwt.interval_ms
src/config/index.ts Re-exports config alongside JwtAdapter
src/db/index.ts Added connectDb() — pings Prisma, logs a clear error and calls process.exit(1) if the database is unreachable
src/index.ts Registers authRouter, applies AuthMiddleware.validateJwt to all four financial route prefixes, calls connectDb() before accepting traffic, starts session cleanup cron
tsconfig.json Added "ts-node": { "files": true } and explicit "files": ["src/types/express.d.ts"] so ts-node picks up the Express augmentation
.env.example Added JWT_SEED, JWT_SESSION_TTL_HOURS, JWT_NONCE_TTL_MS, JWT_CLEANUP_INTERVAL_MS, WALLET_ENCRYPTION_KEY, DB_NAME, DB_PASSWORD
readme.md Added setup instructions, Docker commands, Prisma migration commands

Auth flow

POST /api/auth/challenge   { stellarPubKey }
  └─ server generates nonce (5 min TTL, stored in memory keyed by pubkey)
  └─ returns { nonce, expiresAt }

  [client signs nonce with Freighter wallet]

POST /api/auth/verify      { stellarPubKey, signature (base64) }
  └─ look up nonce → check expiry → verify Stellar signature
  └─ consume nonce (replay attack prevention)
  └─ create user + empty portfolio if first login
  └─ issue JWT (24h)
  └─ persist session row in DB
  └─ returns { token, userId, expiresAt }

POST /api/auth/logout      Authorization: Bearer <token>
  └─ deletes session row → token immediately dead

Protected routes

AuthMiddleware.validateJwt is applied to:

  • POST /api/portfolio
  • POST /api/transactions
  • POST /api/deposit
  • POST /api/withdraw

The middleware validates the JWT signature, then checks that the session exists in the database and has not expired. A missing or stale row always returns 401.


Security properties

Property Mechanism
Stellar ownership proof Keypair.verify() on raw UTF-8 nonce bytes
Replay attack prevention Nonce deleted immediately after first successful verify
Session invalidation Logout deletes session row — JWT alone is not sufficient
Expired session cleanup Lazy (on each request) + daily cron
Token strength JWT_SEED = 256-bit random secret
Nonce strength 32 random bytes → 64 hex chars

Testing

19 unit tests, 0 failures (npm test -- auth):

Challenge

  • ✅ Returns 400 — missing stellarPubKey
  • ✅ Returns 400 — invalid Stellar public key format
  • ✅ Returns 200 with nonce and expiresAt
  • ✅ Overwrites stale nonce on second call

Verify

  • ✅ Returns 400 — missing params
  • ✅ Returns 401 — no active challenge
  • ✅ Returns 401 — expired nonce
  • ✅ Returns 401 — tampered / invalid signature
  • ✅ Returns 200 — happy path (real Stellar keypair sign)
  • ✅ Returns 401 on replay — nonce consumed after first use
  • ✅ Auto-creates user when none exists
  • ✅ Does not create duplicate user

AuthMiddleware

  • ✅ Returns 401 — missing Authorization header
  • ✅ Returns 401 — not a Bearer token
  • ✅ Returns 401 — invalid JWT signature
  • ✅ Returns 401 — session not in DB
  • ✅ Returns 401 — session expired (also deletes stale row)
  • ✅ Calls next(), attaches req.userId and req.stellarPubKey

Logout

  • ✅ Deletes session row, returns 200

Refactoring decisions

StellarVerification extracted from controller

The controller had inline helper functions (purgeExpiredNonces, verifyStellarSignature, resolveNetwork) and owned the nonce store directly. These are Stellar domain concerns, not HTTP concerns. They were moved to src/utils/stellar/stellar-verification.ts as a class that owns the Map and exposes a clean interface. The controller imports and delegates to the singleton.

connectDb() extracted from index.ts

Startup DB connectivity logic was moved into src/db/index.ts alongside the Prisma singleton. index.ts now calls a single await connectDb() line — startup concerns belong to the DB layer.

express.d.ts augmentation fix

ts-node does not automatically load .d.ts files reachable only via the include glob — it only follows explicit import chains. Fixed by:

  1. export {} in express.d.ts (makes it a module augmentation, not an ambient declaration that would shadow all Express types)
  2. "ts-node": { "files": true } + explicit "files": ["src/types/express.d.ts"] in tsconfig.json

How to test locally

# 1. Start Postgres
docker compose up -d

# 2. Copy env and set secrets
copy .env.example .env
# edit .env: DB_PASSWORD, JWT_SEED, WALLET_ENCRYPTION_KEY

# 3. Apply migrations
npx prisma migrate dev --name init

# 4. Run the server
npm run dev

# 5. Run tests
npm test

@robertocarlous
Copy link
Copy Markdown
Contributor

Hello @Joaco2603 Pls pull main branch and sync with your branch and run ci to ensure it pass, so i can merge,thank you

@Joaco2603
Copy link
Copy Markdown
Contributor Author

Hi @robertocarlous I just synced with the main version and everything is up to date. The integration looks good, so we're ready for the merge. Please keep an eye out for any further modifications.

@robertocarlous
Copy link
Copy Markdown
Contributor

Nice implementation

@robertocarlous robertocarlous merged commit f6d2a29 into Neurowealth:main Mar 6, 2026
1 check 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.

[backend] [security] Implement authentication middleware and session management

2 participants