When storing a JWT for authentication in a web application, my first instinct would be to store it as a "hardened cookie", meaning all the required flags such as "HttpOnly" and "Secure" being set. This would still allow me to make use of XHR, as XHR requests to my own domain with
.withCredentials(true) would still include the cookie, while also making sure that an attacker with XSS capabilities wouldn't be able to steal the token.
This attack occurs when a token has been intercepted/stolen by an attacker and they use it to gain access to the system using targeted user identity.
How to Prevent
A way to prevent it is to add a "user context" in the token. A user context will be composed of the following information:
A random string that will be generated during the authentication phase. It will be sent to the client as an hardened cookie (flags: HttpOnly + Secure + SameSite + cookie prefixes). A SHA256 hash of the random string will be stored in the token (instead of the raw value) in order to prevent any XSS issues allowing the attacker to read the random string value and setting the expected cookie.
IP addresses should not be used because there are some legitimate situations in which the IP address can change during the same session. For example, when an user accesses an application through their mobile device and the mobile operator changes during the exchange, then the IP address may (often) change. Moreover, using the IP address can potentially cause issues with European GDPR compliance.
During token validation, if the received token does not contain the right context (for example, if it has been replayed), then it must be rejected.
When looking at the provided code example, this essentially means the token itself is stored in an insecure way, but the token is associated with a "fingerprint", which is stored in a secure way.
This raises the following question for me: If the JWT is useless without the fingerprint cookie, then every request where the JWT is being used also has to include a hardened cookie. Therefore, in order to "use" the JWT, a hardened cookie needs to be used. Why not store the JWT directly in the hardened cookie? Why add this extra convoluted step?