I was implementing a new authentication setup for Duplicati where I was using JSON Web Tokens as the underlying token format. By chance I happened to also read an article about using JWT that concluded that you do not need them for small sites. The argument essentially boiled down to: “All your requests are hitting the database anyway, and since you are not Google-scale you do not have performance issues with this.”
I think most of the arguments in the article are actually correct, but it misses a very important aspect: security.
With anything related to security it is virtually impossible to anticipate every single scenario in advance, and this gives attackers an advantage. If you do not use JWT, you are essentially betting on your solution being smarter than people hired specifically to create a secure authentication and authorization system.
The above article recommends “just using a ‘normal’ opaque session token and storing it in the database”. That is certainly easier to implement and understand: just create a random string, and check that the user sends the correct string. What could go wrong?
Don’t store passwords in your database
In the early days of computers it was common to store usernames and passwords in the database. It is so simple: just check if the user has provided the same username and password as is recorded in the database, and grant access. However, we learned through many, many, many sad stories that this is a terrible security practice, because a leaked database gives full access to any account with no real way to recover, other than replacing all customer passwords.
The suggestion of using an “normal opaque session token” is essentially a variation of storing the password in the database. If an attacker obtains a copy of the database, they will be able to use the session token to impersonate all users. (Of course, ANYTHING is better than storing the plain-text password. Please never, ever, do that!)
However, storing the token is just part of a real solution. A real-life implementation would need to handle token expiration and rotation to mitigate token leaks. Interestingly, this line of thinking is already implemented in JWT libraries, so you can avoid the headache of trying to cover all possible ways to abuse an expiration system by using something that is vetted by some of the world’s largest tech companies. But not storing your tokens in the database is just the first improvement.
Don’t store tokens in your browser
Sadly it is still somewhat common practice to use cookies to store authentication tokens in the browser, which opens the door to a myriad of possible attack vectors, including XSS and CSRF/XSRF, browser hijacking, cookie leaks, and more.
If you follow the practice of using a “normal opaque session token” to do your authentication, you need to use either local storage or cookies to preserve your session token, which opens your users up to these attacks.
Additionally, cookie storage can also be retrieved from user machines if they are infected or a browser defect is found, so it’s important that the tokens are short-lived and preferably not stored in the browser. But how do you authenticate without storing a token?
How to not store a token
To solve the problem of authenticating without a stored token, we can turn to SPAs and use the ability to navigate a site without reloading the page. With an SPA it is possible to obtain the token once and store it in memory. Only when the user reloads the page is the token lost and needs to be obtained again. If your site or service is not an SPA, it may be required to store a persistent token (we will get back to that), but this should never be the token used to access the resource.
How JWT protects against database leaks
If you generate a JWT and store it in the database, you are essentially using the “normal opaque session token” model, but the point of JWT is that they should NOT be stored. Since the JWT is signed with a key it is possible to validate the token with nothing more than the signing key. This is good because it means that your database can no longer leak the credentials or tokens, because they are not there to begin with. Instead, the signing key is now the single secret that needs to be protected, and since it is usually very small, it can be kept in memory or on dedicated hardware.
Persistent logins with JWT
It’s possible for your users to stay logged in beyond a session by using a Refresh Token. Essentially, you create a JWT with a longer expiration and use this to obtain an Access Token. Since JWT supports encoding the token type and expiration, this works without any special effort. Simply exchange the login credentials for a Refresh Token, and the user can obtain new Access Tokens until the Refresh Token expires, without providing the initial credentials again.
This simple separation of Access and Refresh tokens is especially important for use in browsers, because exploits may cause the browser to emit a request the user has not initiated. If the Access and Refresh tokens are the same, there is a chance that the exploit somehow manages to send the token along with the request (e.g., as a cookie with CSRF) which would let the attack succeed. If the Access token is only stored in memory and explicitly added to requests, it is much harder for the attacker to obtain and attach the token to a request. Since the risk is higher for the persistent Refresh Token it’s important that it can only be used to obtain an Access Token, because this requires the attacker to also read the results of the request and later attach the Access Token. It is also important that the Refresh Token is limited as much as possible, specifically that it is NOT accessible from any scripts and ONLY sent to the endpoint that issues the Access Token.
Securing persistent logins with JWT
Since the Refresh Token has a longer life, it needs to be stored in the browser (or client application), making the token a potential target for account hijacking. To avoid this, the Refresh Token needs to be versioned, which can be done with a simple counter. The database keeps a unique id and a counter for each issued Refresh Token. On each use, the counter is incremented and a new Refresh Token is issued.
If the user attempts to use a valid Refresh Token with a counter that does not match the recorded counter, all the user’s tokens can be invalidated, optionally informing the user of the incident.
This simple counter makes it more difficult to obtain a working token, because an attacker would need to grab the token and use it before the counter changes and before the user activates the refresh token. If the counter has a mismatch both the attacker and user are logged out.
Although this adds “something” to the database, it does not add login information. An attacker cannot compromise accounts with the database contents, as it contains no tokens—only counters. An attacker would still need to obtain the signing key to compromise the system. Changing the signing key will immediately revoke all existing tokens, regardless of the database contents.
The real reason you should use JWT
Technically you don’t need JWT/JWE to achieve any of the above properties. You can implement an expiration, a counter, token types, etc. in simple JSON, XML or text. But you are then effectively rolling out your own non-standard version of JWT, and in a sense betting that you can do a better job than Google and their security teams.
Because JWT is a standard, you can use any of the existing libraries and stand on the shoulders of others who have spent significant effort ensuring that loopholes are closed. And better yet: your developers can be familiar with one way of handling logins across applications and organizations.
In short: you should definitely use JWT, not for performance, but for security!
The Duplicati implementation of JWT
The implementation is ready to be merged and is (almost) contained in two files:
- Generating and validating tokens: JWTProvider.cs
- Implementing the web-interface: Auth.cs
As this is MIT licensed, feel free to copy or use in other projects. And comments on the PR or in the forum are always welcome!