Session → JWT Auth Migration
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
| CSRF risk | Active vulnerability |
| Document exploit | Unpatched |
| Mobile session bugs | Recurring |
Stateless JWT with backward-compatible rollout and full vulnerability closure
- 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
- API accepts both session cookies & JWT
- Existing sessions stay valid
- Transparent JWT migration on next login
- Zero forced logouts or service interruption
- 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
- iOS Secure Enclave, Android KeyStore storage
- Automatic silent token refresh
- Token refresh queue serializes concurrent operations
- Prevents race conditions under high API load
- 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
From fragile to enterprise-grade
Every known session-related bug across all three platforms was resolved. No stale session errors, no authentication failures post-migration.
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.
Comprehensive testing across all platforms and edge cases ensured zero regressions in production. No rollback required.
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.
Architectural choices, tradeoffs, and what nearly broke production
JWT + refresh tokens, not OAuth2
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
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
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
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.
Cost · Risk · Tradeoffs
Estimated values based on production metrics and monitoring data
Transformative Results: Before vs After
- Session-based authCSRF vulnerable
- Stateful - hard to scaleScaling ceiling
- Sticky sessionsFrequent timeouts
- Manual token revocationError-prone
- Inconsistent across appsPoor UX
- JWT-based authCSRF eliminated
- Stateless & scalableAuto horizontal scale
- No session storeZero timeouts
- Instant token revocationSecure & reliable
- Unified across platformsConsistent UX
What was used
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.