Background
Sorry for this question being a bit open-ended, but I'm just trying to understand how this works and I'm new to this field.
I'm building a SPA backed by a (Apollo) server. This question pertains to traditional authentication using a JWT Bearer token. I'm gonna assume the server has a valid TLS certificate.
Question
I'm going to start by writing what I understand, please correct me if I get anything wrong. Cheers!
User signs up. We send the SPA an access token with some metadata (e.g. exp) and this is stored in an httpOnly
(to prevent XSS), SameSite=strict
(to prevent CSRF), secure
(to prevent MITM attacks) cookie. This is then sent with every request for authentication without having to query the database, and if we attach roles/scopes to the JWT payload, even for authorization without having to query the user database.
The first problem arises when a user tries to log out.
Question 1
What is the best practice for logging out users with httpOnly cookies? Here I read that best practice is to set two cookies, one without httpOnly (I'm guessing with the same contents (JWT)?) and require both in server authentication logic. When a user logs out, we delete the non-httpOnly one and that effectively logs the user out.
Question 2
How to handle multi-device logins? I'm guessing that the JWTs don't have anything to identify the device, so just issue a new token in a cookie.
So far so good.
Now, under the assumption that the above token never leaks, I believe this is a secure system. However, in reality things are not so simple. Somebody can quickly copy cookie data from an unattended computer. This can even be done using a USB-stick script, since cookies are just files in the filesystem.
Question 3
What are the ways to mitigate this? Here are some more questions, together with my armchair solutions :)
3.1: Do browsers have an API to securely encrypt a cookie? If so, we could encrypt the cookies. I'm guessing they don't.
3.2: I had this whole idea of using subnet masks and IP addresses to uniquely identify devices. But it probably won't work - I'm assuming subnet masks are not carried in http requests like IP addresses, and doing it in js would be at the mercy of the attacker. Finally, the pair (IP, subnet mask) is not a very good identifier for a device because after disconnecting, another device can assume that subnet mask. F*ck.
3.3: Use short-lived JWTs. A bit of a hacky solution imo. We set the JWT exp to 15-30 min and assume that in that time, an attacker can't cause much damage
. Critical operations like deleting an account should still require password (which will be sent over https), limiting the scope of the attack. After 15 min, the user will be prompted to log back in and can revert all the effects or contact support to remove them.
However, a new problem arises: we don't want users to have to login every 15 min. This is where my understanding ends:
3.3.1: Use a long-lived refresh token that is stored as a cookie - well doesn't really change much.
3.3.2: Use a long-lived refresh token in the db. Ok, seems fair. As soon as a user spots malicious behavior in their account, they can contact support, all refresh tokens will be deleted and an attacker will have <15 min remaining. Actually, we're just interested in whether or not there was a breach, so we can just use a boolean; why bother with a refresh token?
The problem imho is an attacker still gets view-access, forever. So we still need to combine this with some identification of the device (User-Agent, IP address...) introducing additional complexity.
It seems the best solution, for a non-critical (banking) app is to just use long-lived access token. I'll try to justify that decision with two arguments:
3.3.3: If somebody has physical access to your device, they can often do much worse things then copy cookies.
3.3.4: Facebook seems to use 6-month access tokens? At least that's what it seems on the face of it: I went to fb.com, deleted my c_user
cookie, cmd+r, login, and a new one is created in 6 months minus some change. But I wasn't able to copy the cookies in a working manner from Brave to Chrome. Am I doing something wrong or is there an actual good way to prevent such an attack (without querying the db on every request)?
Closing
Sorry for the long text but there is so much fud and incomplete answers regarding security that I just want to make sure I'm doing everything right. If anyone has comments, or partial answers to what I wrote I'll be super grateful. I'm really excited to learn about this new field of web security!