Session IP and user-agent were enforced as an exact-match gate on
every authenticated request and on refresh: any mismatch threw
Invalid session binding / Session binding mismatch and forced a
logout. In production this made the app nearly unusable, because
real users' IPs change constantly and normally in ways that have
nothing to do with account theft — mobile network handovers,
corporate NAT pools, VPNs, CDN edge changes, Wi-Fi/cellular
switching.
The still-enforced single-use rotating session nonce plus
refresh-token rotation and revocation already prevent a stolen
token from being replayed on another device, so the hard IP/UA gate
was mostly adding false-positive lockouts rather than real security.
Now IP and user-agent changes are recorded as a SESSION_BINDING_DRIFT
abuse event (feeding the existing risk-scoring system) instead of
rejecting the request, so the security signal isn't lost, it's just
no longer a blanket block on legitimate use.
Without Express's trust-proxy setting, req.ip resolves to the load
balancer's own connecting IP for every request in production, not
the real client IP. That silently broke per-IP rate limiting: every
user behind the same proxy shared one 100-requests-per-60-seconds
throttler bucket instead of getting their own, so any real traffic
volume (the dashboard alone fires ~6 requests per load) could 429
everyone, including on login/registration.
The register form collects a full name, but RegisterDto rejected it as
an unknown field under the global whitelist validation pipe, so
registration always failed 400 whenever a name was entered.