Problems Encountered When Implementing JWT Authentication
Understanding JWT Tokens
JWT was originally designed to enable secure data exchange across organizational boundaries. Because JWT alone cannot guarantee full security, JWE was introduced as an extension. However, the JWE spec defines many attributes and naming conventions that most projects don't need — I think it's fine to borrow the spirit of the approach without following the spec to the letter.
Goals
- Mobile app can share the same authentication flow and API as the web, without maintaining two separate systems.
- Stateless APIs make it easier to scale servers horizontally — each additional server contributes proportionally to overall throughput.
- Resolves Same-Site Cookie issues (only relevant on the product side, not the platform side).
Risks
- Security is fully controlled by the developer.
Existing System Dependencies on Session
- Login state
- Failed login attempt count
- IP blocking
- CAPTCHA
Existing System Dependencies on Cookie
- Sticky cookie
- Data cookie
- SignalR uses .NET Forms Authentication (cookie-based) — switch to passing the JWT token on connect instead
JWT Validation Mechanism
To prevent token forgery, the token is written to Redis upon login. Every request must be validated against Redis.
Where to Store the JWT Token
| Approach | Issues |
|---|---|
| LocalStorage | XSS vulnerability; doesn't work in mobile private/incognito mode; not supported on older mobile browsers |
| LocalStorage → SessionStorage | LocalStorage has compatibility issues |
| redux-state-sync | Requires EA tech review process — deferred for now |
| In-Memory (chosen) | Cross-window state sync is more complex to implement, but has the best compatibility |
Note: Both cookies and LocalStorage create physical files on the client side. Modern browsers encrypt these files, but they can potentially be cracked. SessionStorage is more secure than LocalStorage. Keeping the token in memory (no persistence to disk) is the safest option.
JWT Token Length
The existing online user count is calculated from the SessionId column in the login table, which currently has a maximum length of 100 characters. Standard JWTs are typically longer than 100 characters, so database columns need to be adjusted accordingly.
Cross-Window Race Condition on Token Refresh
The desktop web app has a "window.open" feature for viewing account history. From Angular's perspective, this spawns an independent app instance. Both the parent window and the child window can trigger token refresh simultaneously, causing a race condition.
My approach: child windows never refresh the token themselves. Instead, they use postMessage to notify the parent window to refresh. Subsequent requests are queued in the child window until the parent completes the refresh and signals back to unlock the queue. If the wait exceeds a timeout threshold, the queue is released via RxJS timeout.
Forced Logout (Kick) Logic
The JWT spec doesn't define forced logout behavior — all kick logic must be implemented manually:
- Idle timeout (no trades placed)
- Token expiry
- Double login (duplicate session)
- Manual logout
- Session not properly closed
- Maintenance kick (from the BO site)
The kick mechanism works by clearing the token in Redis, effectively invalidating it. Log the kick reason for easier debugging later.
Making JWT More Secure
- The site must use HTTPS.
- Shorten the access token lifetime.
- Bind the access token to a browser fingerprint (Device ID): https://fingerprintjs.com/?fbclid=IwAR1I0nRPyiJJUHosyXa80GwJi45-4tHjxV5uZPnTBks-XT3Kp3-tyi1ln1M
- Disable DevTools access (F12) — leave a back door for yourself.
- Log member login history.
- Bind to IP — though clients may switch networks and mobile IPs can change frequently, this could still be a valid policy as long as the business team accepts that IP changes require re-login. Show the logout reason when it happens.
Coexisting Session/Cookie Fallback
After going live, if a critical issue arises that takes a long time to fix, it's helpful to be able to quickly roll back to the old mechanism. A coexistence mode is retained where a config flag determines whether Autofac injects JWT or Session logic:
if (IsJWTAuthentication)
{
builder.RegisterType<JWTSessionData>().As<ISessionData>();
builder.RegisterType<JWTAuthenticationBLL>().As<IAuthenticationBLL>();
}
else
{
builder.RegisterType<SessionData>().As<ISessionData>();
builder.RegisterType<AuthenticationBLL>().As<IAuthenticationBLL>();
}
Rough Scope of Work
- Eliminate Session dependencies
- Eliminate Cookie dependencies
- Implement JWT token issuance
- Implement JWT login/authentication
- Implement forced logout (kick) functionality
- Implement token refresh
- Swap out Session/Cookie mechanism in API layer
Addendum
Is HTTPS Always Secure?
Not necessarily. The following scenarios can make HTTPS insecure:
- Using outdated encryption algorithms
- Bugs in the cryptographic library implementation
- The Certificate Authority (CA) is compromised
- Private key leakage
- Government-level interception (e.g., FBI)
Side Effects of Binding JWT to IP
Client IPs can change when users switch networks, and mobile IPs are particularly volatile. Binding a token to an IP may not be appropriate for all use cases. That said, it can be a valid policy — as long as the business team agrees that any IP change triggers a forced logout with a reason shown to the user.
Additional Findings
- Redux DevTools was not disabled in the production build.





























Comments