Senior Backend Engineer · Security Engineering · Pickles Asia (Feb–Mar 2025)

Session → JWT Auth Migration

Pickles Asia's Auction Management System ran on session-based authentication while the mobile app had already adopted JWT - creating a fragmented, inconsistent security posture across three client platforms. A security audit surfaced two active vulnerabilities: CSRF exposure inherent to session-based flows, and an open-document exploit that allowed unauthenticated access to restricted documents. The session architecture was also the root blocker preventing a Node.js runtime upgrade from the end-of-life v12. I led the full migration to JWT across the Vue.js web app, React Native mobile app, and the Node.js backend API - with a backward-compatible rollout window that kept existing sessions valid through the transition. Zero regressions. Both vulnerabilities closed. Node.js v12 to v21 unblocked.

auth.session → jwtShipped
100%
Resolved
3
Platforms
0
Post-bugs
v12→v21
Node.js
session era → jwt · 0 bugs ↑
The Challenge

Session-based auth at its security and scalability limits

The AMS admin portal and API used session-based authentication, while the mobile app already ran on JWT - creating a two-tier inconsistency that complicated the codebase and blocked infrastructure work. Sessions are inherently stateful: they require a shared session store, prevent the API and admin web from running in separate ECS pods, and introduce CSRF attack vectors that token-based auth eliminates entirely. More critically, a security audit identified two live vulnerabilities. The first was CSRF exposure from session cookies. The second was an open-document view exploit that allowed unauthenticated users to retrieve restricted documents by manipulating URL parameters. Both traced directly to the session-based authentication model. And the same architecture was the root cause blocking a long-overdue Node.js runtime upgrade from the severely outdated v12.

  • CSRF vulnerabilities inherent to session-based cookie authentication across all web clients
  • Open-document exploit - unauthenticated URL manipulation allowed access to restricted documents
  • Session store scaling complexity as platform traffic and concurrent admin sessions grew
  • React Native app session handling fragile with recurring expiry bugs reported in production
  • Authentication fragmented across three clients: sessions on web, JWT on mobile - inconsistent security posture

auth_system.security_auditFailed
CSRF
Attack vector
open
Doc exploit
stateful
Session scaling
CSRF riskActive vulnerability
Document exploitUnpatched
Mobile session bugsRecurring
The Solution

Stateless JWT with backward-compatible rollout and full vulnerability closure

1
JWT access and refresh token strategy - Replaced shared session store with stateless tokens:
  • Short-lived access tokens (in-memory on clients)
  • Long-lived refresh tokens (secure storage)
  • API issues & validates tokens without session store
  • Enables stateless horizontal scaling & ECS pod separation
2
Backward-compatible transition window - Two-week grace period for user migration:
  • API accepts both session cookies & JWT
  • Existing sessions stay valid
  • Transparent JWT migration on next login
  • Zero forced logouts or service interruption
3
Document exploit fully closed - Eliminated unauthenticated URL access:
  • Valid JWT claims required on every request
  • Server-side resource-level validation
  • No document accessible without valid, non-expired token
  • URL manipulation exploit fully closed
4
React Native mobile app secure token handling - Secure token storage across mobile platforms:
  • iOS Secure Enclave, Android KeyStore storage
  • Automatic silent token refresh
  • Token refresh queue serializes concurrent operations
  • Prevents race conditions under high API load
5
ECS pod separation and Node.js v12 → v21 upgrade - Stateless JWT unlocked platform modernization:
  • Removed shared session store dependency
  • API & admin web now run in separate ECS pods
  • Node.js v12 → v21 upgrade enabled
  • Platform modernization unblocked after years
Session → JWT: Before vs After
BEFORE · SESSION-BASEDVue.jsSession cookieReact NativeAlready JWTNode APIMixedSession cookieSession AuthServer-side session stateFragmented across 3 clientsNode.js API + Session StoreNode v12 · cannot upgrade⚠ CSRF VULNERABILITY (active)AFTER · STATELESS JWTVue.jsJWT (access)React NativeJWT (unified)Node APIJWT issuerBearer JWTJWT Auth (stateless)Access: in-memory · short-livedRefresh: secure storage · long-livedNode.js API (stateless)Node v12 → v21 unblocked ✓CSRF eliminated ✓MIGRATION STRATEGY2-Week Backward-Compatible Grace PeriodAPI accepts BOTH session tokens + JWT simultaneouslyClients migrated one by one · zero forced cutover · ECS pods separated per serviceCSRFEliminated0 attack surfaceNode v21Unblockedfrom v12ECS podsSeparatedper service3 PlatformsUnified auth1 token typeCSRF eliminated · Node v12→v21 · 3 platforms unified · open-document exploit patchedZero forced downtime · backward-compatible 2-week migration window
Token Lifecycle & Secure Mobile Storage
LOGIN · TOKEN ISSUANCEClientweb + mobilePOST /loginAuth Servicebcrypt verify · sign JWTAccess TokenHS256 · 15 minRefresh Tokenopaque · 7 daysSILENT REFRESH · ACCESS TOKEN EXPIRY FLOWGET /api/dataBearer expired401UnauthorizedInterceptorqueue requestsPOST /auth/refreshrotate refresh tokennew access token → retry all queued requestsMOBILE SECURE TOKEN STORAGEiOSKeychainaccess tokenSecureEnclaverefresh tokenBiometric unlock · token never in plain textAndroidEncryptedSharedPrefaccess tokenAndroid Keystorerefresh tokenAES-256 · key never leaves hardwareLogout · Token Invalidationrefresh token blacklist (Redis) · clear secure storage · revoke all sessionsHS256 · 15-min access · 7-day refresh rotation · 0 post-migration auth regressions
The Results

From fragile to enterprise-grade

100%
Session issues resolved

Every known session-related bug across all three platforms was resolved. No stale session errors, no authentication failures post-migration.

3
Platforms migrated

Vue.js web app, React Native mobile app, and the Node.js backend API all migrated simultaneously with full backward compatibility during the transition window.

0
Post-launch regressions

Comprehensive testing across all platforms and edge cases ensured zero regressions in production. No rollback required.

v12→v21
Node.js upgrade unlocked

The JWT migration enabled separation of the API and admin web into distinct ECS pods - the architectural prerequisite that finally unlocked upgrading Node.js from the severely outdated v12 all the way to v21.

Engineering Decisions

Architectural choices, tradeoffs, and what nearly broke production

JWT + refresh tokens, not OAuth2

Why not use OAuth2 or OpenID Connect?

For an internal system, JWT with refresh token rotation is simpler and faster to implement than a full OAuth2 provider. OAuth2 adds server-to-server complexity we didn't need. JWT + secure refresh token storage + automatic silent refresh gave us the security guarantees without operational overhead.

Secure storage, not localStorage

Why secure token storage instead of localStorage?

localStorage is vulnerable to XSS injection. Access tokens live in memory, refresh tokens in secure storage. On React Native, we use secure enclave on iOS and KeyStore on Android. This two-tier approach balances UX (no re-authentication on refresh) with security (no token theft via script injection).

What broke: Mobile token refresh race condition

What nearly failed during the migration?

During high-frequency API calls, multiple requests could trigger simultaneous token refresh operations on the mobile app. This created a race where an old token would briefly re-issue while a new one was being fetched, causing intermittent 401s. Fixed by implementing a token refresh queue that serialises refresh operations.

Lesson: Race conditions in token lifecycle are easy to miss in testing but cause user-facing bugs in production. Added specific test cases for concurrent refresh scenarios.

Backward compatibility was mission-critical

How did you prevent forced logouts?

The API accepted both session cookies AND JWT for 2 weeks during rollout. Existing sessions stayed valid, new logins got JWT. Once all devices had upgraded, we sunset session support. This transparent migration meant zero user friction and no customer support load.

THE TRADEOFFS

Cost · Risk · Tradeoffs

Estimated values based on production metrics and monitoring data

Cost Impact
~20% ↓
Infrastructure cost reduction
Stateless JWT eliminated session store infrastructure; now an optional Redis cache only
Risk Reduction
high → eliminated
Security risk (CSRF)
Token-based auth with SameSite cookies removes entire CSRF attack vector
open → closed
Security risk (document exploit)
Moved to resource-level JWT validation; unauthenticated users now fully blocked
Technical Tradeoff
increased
Operational complexity
Trade: added token refresh logic. Mitigated by using industry-standard libraries and comprehensive test coverage
Before vs After

Transformative Results: Before vs After

✕Before
  • Session-based authCSRF vulnerable
  • Stateful - hard to scaleScaling ceiling
  • Sticky sessionsFrequent timeouts
  • Manual token revocationError-prone
  • Inconsistent across appsPoor UX
→
✓After
  • JWT-based authCSRF eliminated
  • Stateless & scalableAuto horizontal scale
  • No session storeZero timeouts
  • Instant token revocationSecure & reliable
  • Unified across platformsConsistent UX
⇪Business Impact
100% vulnerabilities fixed
0 post-launch bugs
3 platforms migrated
Tech Stack

What was used

BackendNode.js · Express
Token StrategyJWT (jsonwebtoken)
Web AppVue.js · Axios
Mobile AppReact Native
Also UpdatedReact (admin)
Token StorageSecure Store · Memory
TestingJest · Mocha
CI/CDAzure DevOps

Dealing with a legacy auth system or active security vulnerabilities?

I design zero-downtime security migrations that close vulnerabilities without breaking existing user sessions. Let's talk about what you're working with.