3

I'm trying to create a secure method of authentication for my Node.js REST API that will work both for direct API requests as well as from my React.js web application. I've done some reading on how to properly store authentication tokens to protect against XSS and CSRF, so I'd like to present what I've come up with to see if there are any holes in it, or if it will work at all.


This method will use a combination of JWT, localstorage (React app), and cookies (Node API).

The API has an endpoint /authentication. When called, it will generate a CSRF token and include it in the payload of the JWT. It will then set a cookie (HttpOnly, secure) with the JWT, and return said JWT.


When /authentication is called from my React.js web app, it will receive the JWT, and store just the CSRF token in localstorage.

Then, whenever the app needs to fetch a resource from the API, it will send the CSRF token in the header of each request.


The Node.js app will have a middleware that checks a few things:

  • If the entire JWT is present, it will authenticate. This is the case where users are interacting with the API directly.
  • If it's just the CSRF token, we will first verify the signature of the JWT in the cookie, and then compare the received CSRF with the one in the cookie. If they match, then it will authenticate.

My thinking is that this way, the JWT is never exposed on the web app for any attacks to occur.

Does this model seem secure enough?

2 Answers 2

0

Welcome to the site, Vipulp!

I am not certain if your requirements allow it (apologies if not), but I would recommend using an existing node.js library to do the heavy lifting for you. Something like passport http://www.passportjs.org/docs/ or the like.

This way you don't need to reinvent the wheel, and are less at risk of missing an edge case or including a bug.

0
0

Your approach is probably safe enough, assuming that when you say "If the entire JWT is present..." you mean present in the header, not (just) in a cookie. Similarly, you say

It will then set a cookie (HttpOnly, secure) with the JWT, and return said JWT.

Presumably you mean return the JWT in the response body, as well as in the Set-Cookie header? And presumably your API does this regardless of the client (API client vs. webapp client)? You need to be specific about these things.


However, your approach is complicated, which greatly increases the risk of a vulnerability. Keep it simple. I recommend one of the following approaches instead:

  1. Just transmit the full JWT in a header in both cases. This necessitates storing it in JS-readable storage in the webapp, but lets you do away with cookies entirely (or at least, not for the JWT; you might use them for other things), eliminates any risk of CSRF with any client, and means there's only one canonical location where the JWT is expected to be. The increased risk from having the JWT in JS-readable storage is really quite minimal; JWTs are usually not revocable (there are ways to make them so, but it adds complexity and loses some of the benefits of using JWTs at all so generally they are completely stateless and hence irrevocable), and irrevocable tokens need to have very short lifetimes (a few minutes is typical), so the risk from them being stolen is minimal. In most cases, the attacker can simply keep making requests through the victim's browser without any need to steal the token, whether it's visible to script or not.
  2. Use cookies for the JWT, plus some CSRF protection. API clients are entirely capable of both receiving and sending cookies, and while most non-browser ones won't do so automatically the way browsers do, this is quite trivial to work around for an API client developer. This once again means there's only one place the JWT will ever be looked for in a request, and simplifies your request authorization logic. You do need to have CSRF protection, at least for state-changing requests; the idea of putting a value in the JWT and expecting it in a header is valid, but you could also expect the value in the request body, or simply require the presence of a given custom header without requiring a secret value (e.g. X-CSRF: false); assuming you don't have a completely broken CORS configuration, that is enough to prevent CSRF (browsers won't let script send custom headers cross-origin without a CORS pre-flight explicitly granting that origin the permission to send that header) and is very simple to add at the client and to check at the server.

With that all said, there are possibly other issues you need to consider. What are you planning to do about session lifetimes? Unless you're planning to include a revocation check in your JWT validation logic - which makes JWTs not entirely stateless, losing one of their performance advantages - you will have no way to invalidate JWTs before they expire, so their expiry must never be far away (that is, their lifetime must be short). To avoid a bad user experience, you'll want a "refresh token" that is longer-lived but not checked on every request (generally it's only checked, and often only sent, on requests to a specific endpoint such as /refresh). The refresh token is unique to each user session, and can be used to obtain a new valid JWT even if the client's old JWT has expired (assuming that the refresh token hasn't been revoked, e.g. by the user logging out or an administrator disabling their account). Refresh tokens absolutely must be revocable, so they're generally opaque random tokens rather than JWTs or other typically-stateless tokens. They're longer-lived, so it's more important to keep them secret even if the client is compromised; a Secure + HttpOnly cookie is a good choice.

If you go with anti-CSRF tokens (transmitted in the JWT, or elsewhere), make sure they are unique per user - ideally unique per login session - and unpredictable. A predictable (either because it's short/low-entropy, consistent, or generated from an insecure PRNG) anti-CSRF token can't reliably prevent the attacker from making CSRF attacks (though if the token is additionally transmitted in a header, the need for CORS pre-flights can still prevent CSRF even if the value to be sent is known to the attacker).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .