4

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!

Dominik Teiml
  • 505
  • 1
  • 4
  • 12

1 Answers1

3

This question is a little too broad, but let me try and answer a few points.

  1. If you set a cookie without httpOnly and with the same JWT, it makes that vulnerable to XSS, so it doesn't make any sense to have the httpOnly one too. You could just make a request to the server and ask it to remove the cookie for you instead. Also see below.

  2. Sure, the same user from a different device is just another JWT.

  3. This threat is not specific to JWTs, a plain old session id might be stolen the same way. Encrypting it does not help, because then the encrypted version would be stolen, and that's all that'd be needed for authentication. Also the key would have to be available wherever the token is stolen from. You mostly don't have to deal with this, the physical security of clients is usually beyond the scope of a typical web application. What you can and should do is issue short-lived access tokens with long-lived refresh tokens, and store them differently.

A reasonably secure way to do this thing for many usecases:

  • Don't use meaningful tokens (with information beyond a large random number) if a plain old session id (~a large random number) is sufficient. It very often is.
  • Use different origins for authentication (issuing tokens) and services (using tokens for authentication). OpenID Connect (and Oauth2 to some extent) have these concepts of identity providers and service providers.
  • The access token can be stored localstorage for the service origin, allowing your javascript access to identity info and claims, and accepting the risk of potential XSS having access. This might not be the case in all applications, so you have to assess this risk! Also storing the token in a cookie will make the application vulnerable to CSRF, and SameSite will only work in the newest browsers (released in about the past year), that might not be enough. Whether this is a problem for you again depends on your usecase and threat model.
  • The refresh token can be stored in a httpOnly cookie for the identity provider origin. So you would have to implement proper error handling in your applications to try and get a new access token from the identity provider if the old one doesn't work anymore.
  • All of this should be implemented in a well-known and well-tested library, because it's not straightforward to get it right. There are great identity solutions (both paid and free) that you can and should use.
Gabor Lengyel
  • 14,129
  • 4
  • 32
  • 59