|
| 1 | +# Authentication Persistence |
| 2 | + |
| 3 | +In this lesson, we will tackle how to persist user login, or registration between requests. Learning objectives are: |
| 4 | + |
| 5 | +- Difference between stateful and stateless persistence |
| 6 | +- Implementing stateful persistence using express-sessions |
| 7 | +- Implementing stateless identifiers using JSON Web Tokens (JWT) |
| 8 | + |
| 9 | +## **Stateful Session Storage** |
| 10 | + |
| 11 | +In web servers, sessions are server-side temporary storage tools. They are used for a variety of use cases to store temporary data between requests. They use different storage adapters, like RAM, disk, databases, in-memory cache, etc. to save the temporary data. |
| 12 | + |
| 13 | +They are different than database data persistent in that session data are simple key-value stores that can be invalidated and become redundant easily. They are some handy copies (cache) of the original data from the database that are supposedly easier and faster to access and deleting them won't affect the server. |
| 14 | + |
| 15 | +In Express.js, we can use the library [express-session](https://www.npmjs.com/package/express-session) to create session storage. It can be plugged into the Express app as a middleware that attaches a `session` object to any request that comes to the server. |
| 16 | + |
| 17 | +```js |
| 18 | +// ./app.js |
| 19 | + |
| 20 | +const express = require("express"); |
| 21 | +const sessions = require("express-session"); |
| 22 | + |
| 23 | +// Init our express app |
| 24 | +const app = express(); |
| 25 | + |
| 26 | +// Below is where we introduce encrypted sessions |
| 27 | +// Make sure your app secret is unique and |
| 28 | +// defined by cryptographic random generator. |
| 29 | +// It has in-built ability to parse and use cookies |
| 30 | +app.use( |
| 31 | + sessions({ |
| 32 | + secret: "MY_APP_SECRET", |
| 33 | + cookie: { secure: false }, // make sure to change this to true on production code |
| 34 | + }) |
| 35 | +); |
| 36 | + |
| 37 | +let listener = app.listen(5000, function () { |
| 38 | + console.log("Listening on port " + listener.address().port); |
| 39 | +}); |
| 40 | +``` |
| 41 | + |
| 42 | +In the code snippet above, we configured the session storage using `app.use()` and created a new session instance with the configuration below: |
| 43 | + |
| 44 | +- `secret`: a token that will be used to encrypt the data and cookies. It can also be a universal `APP_SECRET` defined in the environment variables. This must be a random secret generated by secure random generators. |
| 45 | +- `cookie`: cookie parameters that will be set in the client. Because our development environment doesn't have HTTPs, we turned off `secure`. In a production environment, the cookie should only be sent in secure contexts so secure should be true. |
| 46 | + |
| 47 | +This initialization would eventually activate the `session` object in each request, and a `Set-Cookie` header to each response when a session is created. The cookie will contain the `sessionID` that the server can use to read the session storage and grab the session data. |
| 48 | + |
| 49 | +When a cookie is set in the client or browser, the client will always send that cookie with subsequent requests to the original server that issued the cookie. The cookie will be sent as part of the request headers. That's why it is important to ensure it is only sent in secure contexts, so it is encrypted and an eavesdropper can't [hijack the session](https://en.wikipedia.org/wiki/Session_hijacking). |
| 50 | + |
| 51 | + |
| 52 | + |
| 53 | +This can be used to persist authentication as well. This is often done when a user logs in and activates the **Remember Me** function. When login validation is carried out successfully, the server should also save a user copy to the session as seen in the previous lesson: |
| 54 | + |
| 55 | +```js |
| 56 | +req.session.user = user; |
| 57 | +``` |
| 58 | + |
| 59 | +And when requesting protected resources, we can check if the request session that is tied to the request cookies, has the user or not. If it has the user as stored in our login logic, it means the request is authentic. |
| 60 | + |
| 61 | +> ⚠️ **Warning**: When using `express-session`, the default server-side session storage, `MemoryStore`, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing. |
| 62 | +
|
| 63 | +> 💡 **Tip**: `express-session` comes with lots of adapters to store sessions as plug-in libraries. Check out [this list](https://github.com/expressjs/session#compatible-session-stores). |
| 64 | +
|
| 65 | +## **Stateless Session Storage** |
| 66 | + |
| 67 | +In stateless session storage, we don't store any temporary data on the server. Thus, the server is state-less. Instead, we issue a session payload in a token that encapsulates the data and sends it to the client for future use in subsequent requests. JWT is often, the way to carry out this process. |
| 68 | + |
| 69 | +JWT is an encoded string that holds **claims** supposed to authenticate and identify a request. |
| 70 | + |
| 71 | +``` |
| 72 | +// Example JSON Web Token |
| 73 | +
|
| 74 | +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTYzMjk0MTc4NCwiZXhwIjoxNjMyOTQ1Mzg0fQ.MVnMhRVLYhaGqtZEX4AiEMrQwFWRvRLp6MATodg6E6A |
| 75 | +``` |
| 76 | + |
| 77 | +This is a string that has 3 parts separated by a dot (.) and `base64` encoded. It can be decoded easily using any `base64` decoder. |
| 78 | + |
| 79 | +```js |
| 80 | +const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTYzMjk0MTc4NCwiZXhwIjoxNjMyOTQ1Mzg0fQ.MVnMhRVLYhaGqtZEX4AiEMrQwFWRvRLp6MATodg6E6A'; |
| 81 | + |
| 82 | +token.split('.').forEach(part => console.log(atob(part))) |
| 83 | + |
| 84 | +/* Output: |
| 85 | +{"typ":"JWT","alg":"HS256"} |
| 86 | +
|
| 87 | +{"sub":"1234567890","name":"John Doe","admin":true,"iat":1632941784,"exp":1632945384} |
| 88 | +
|
| 89 | +1YÌ�KbªÖD_"ÊÐÀU½éèÀ¡Ø: |
| 90 | +``` |
| 91 | +
|
| 92 | +The first part defines the _algorithm_ and the type. The second part is called the payload or _claims_, which identifies the user, issue time, expiry time, and other data. The third part is called the _signature_. |
| 93 | +
|
| 94 | +When a user logs in, the server will issue this token by first defining the claims, signing them, finally encoding and sending them to the client. The client will persist this token somewhere in the browser, most commonly in the `localStorage`. |
| 95 | +
|
| 96 | +To make an authenticated request by the client, it needs to pass this token to the server. Commonly, it is added to the request headers using the `Authorization` header. |
| 97 | +
|
| 98 | + |
| 99 | +
|
| 100 | +### **JWT Validation** |
| 101 | +
|
| 102 | +A middleware library like [express-jwt](https://www.npmjs.com/package/express-jwt) can be used on the server to validate JWTs. |
| 103 | +
|
| 104 | +```js |
| 105 | +// ./app.js |
| 106 | +
|
| 107 | +const express = require("express"); |
| 108 | +const jwt = require("express-jwt"); |
| 109 | +
|
| 110 | +// Init our express app |
| 111 | +const app = express(); |
| 112 | +
|
| 113 | +// Below is where we introduce JWT |
| 114 | +// Make sure your app secret is unique and |
| 115 | +// defined by cryptographic random generator. |
| 116 | +app.use( |
| 117 | + jwt({ |
| 118 | + secret: "MY_APP_SECRET", |
| 119 | + algorithms: ["HS256"], |
| 120 | + }) |
| 121 | +); |
| 122 | +
|
| 123 | +let listener = app.listen(5000, function () { |
| 124 | + console.log("Listening on port " + listener.address().port); |
| 125 | +}); |
| 126 | +``` |
| 127 | +
|
| 128 | +With this, we've set up a JWT validator for every request, that reads the `Authorization` header, validates the token against the signature and expiration date, scope, etc, and parses the claims to identify the user. |
| 129 | +
|
| 130 | +Then e can simply access the user object in the request as follows: |
| 131 | +
|
| 132 | +```js |
| 133 | +const user = req.user; |
| 134 | +``` |
| 135 | +
|
| 136 | +### **Issuing JWT** |
| 137 | +
|
| 138 | +To issue JWT, your authentication server can use a library [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) to issue the token as follows: |
| 139 | +
|
| 140 | +```js |
| 141 | +// ./routes/auth.js |
| 142 | +... |
| 143 | +const jwt = require('jsonwebtoken'); |
| 144 | +
|
| 145 | +// Signing secret |
| 146 | +const SECRET = 'MY_APP_SECRET'; |
| 147 | +
|
| 148 | +
|
| 149 | +router.post('/login', async (req, res) => { |
| 150 | + const {username, password} = req.body; |
| 151 | +
|
| 152 | + const user = await User.login(username, password); |
| 153 | +
|
| 154 | + if(user) { |
| 155 | + // User is valid, issue JWT |
| 156 | + const claims = { |
| 157 | + sub: user.id, |
| 158 | + name: user.name, |
| 159 | + username: user.username |
| 160 | + } |
| 161 | +
|
| 162 | + const token = jwt.sign(claims, SECRET, { expiresIn: "2h" }); |
| 163 | +
|
| 164 | + res.json({ token }) |
| 165 | + } else { |
| 166 | + res.status(400).render('login') |
| 167 | + } |
| 168 | +}) |
| 169 | +
|
| 170 | +... |
| 171 | +``` |
| 172 | +
|
| 173 | +> ⚠️ **Warning**: Please note that in the code above, for clarity, the `SECRET` was hard-coded and defined inline. **Never do this**. Usually, your `app-secret` is stored in environment variables and **never** shipped with the code. |
| 174 | +
|
| 175 | +> 💡 **TIP**: Always keep your tokens short-lived as they are, by design, harder to revoke. The token by default will stay functional until it expires. |
| 176 | +
|
| 177 | +### **JWT Signature** |
| 178 | +
|
| 179 | +You're probably wondering: if JWTs can be easily decoded and their payload can be seen, how do we make sure that someone doesn't pass any payload to authenticate a request? The signature is the answer. |
| 180 | +
|
| 181 | +When issuing JWTs, we take the claims and encrypt them using the `app-secret` key. Then attach that signature to the JWT as a `base64` encoded string. Remember that the `app-secret` is only known by your app. |
| 182 | +
|
| 183 | +When the JWT is passed back to the server in the header, we check the claims, and sign them again using our `app-secret`, then compare this signature with the signature provided by the token. If they match, that means the JWT is indeed issued by this app. Otherwise, it is fake. |
| 184 | +
|
| 185 | +## **Pros and Cons of JSON Web Tokens** |
| 186 | +
|
| 187 | +JWTs are becoming more and more ubiquitous. Customer identity and access management (CIAM) providers everywhere are pushing JWTs as the silver bullet for everything. JWTs are pretty cool, but let’s talk about some of the downsides of JWTs and some of their strong benefits. |
| 188 | +
|
| 189 | +### PRO: JWTs are portable units of identity |
| 190 | +
|
| 191 | +That means they contain identity information as JSON and can be passed around to services and applications. Any service or application can verify a JWT itself. The service/application receiving a JWT doesn’t need to ask the identity provider that generated the JWT if it is valid or check any database for it. Once a JWT is verified, the service or application can use the data inside it to take action on behalf of the user. Plus, it works across different clients and domains. |
| 192 | +
|
| 193 | +### PRO: Token-based Authentication is more Scalable and Efficient |
| 194 | +
|
| 195 | +Imagine your app has billions of users, and each of them creates a session. Eventually, the session storage will become pretty big and harder to maintain. |
| 196 | +
|
| 197 | +Tokens on the other hand are required to be stored on the user’s end, they offer a scalable solution. |
| 198 | +
|
| 199 | +Moreover, the server just needs to create and verify the tokens along with the information, which means that maintaining more users on a website or application at once is possible without any hassle. |
| 200 | +
|
| 201 | +### PRO: Flexibility and Performance |
| 202 | +
|
| 203 | +Flexibility and enhanced overall performance are other important aspects when it comes to token-based authentication as they can be used across multiple servers and they can offer authentication for diverse websites and applications at once. |
| 204 | +
|
| 205 | +This helps in encouraging more collaboration opportunities between enterprises and platforms for a flawless experience. |
| 206 | +
|
| 207 | +### PRO: Tokens Offer Robust Security |
| 208 | +
|
| 209 | +Since tokens like JWT are stateless, only a secret key can validate it when received at a server-side application, which was used to create it. |
| 210 | +
|
| 211 | +Hence they’re considered the best and the most secure way of offering authentication. |
| 212 | +
|
| 213 | +Tokens act as a storage for the user’s credentials and when the token travels between the server or the web browser, the stored credentials are never compromised. |
| 214 | +
|
| 215 | +### CON: Compromised Secret Key |
| 216 | +
|
| 217 | +The best and the worst thing about JWT is that it relies on just one Key. |
| 218 | +
|
| 219 | +Consider that this Key could be leaked by a careless or rogue developer/administrator, and then the whole system is compromised! The attacker (who has access to the Key) can easily access all user data if he has the user-id which can easily be acquired. |
| 220 | +
|
| 221 | +The only way to recover from this point is to generate a new Key (Key-pair) that will be used across systems from here on. This would mean that all the existing client tokens are invalidated, and every user would have to login again. Imagine if one day all Facebook users are suddenly logged out. |
| 222 | +
|
| 223 | +### CON: JWTs expire at specific intervals |
| 224 | +
|
| 225 | +When a JWT is created it is given a specific expiration instant. The life of a JWT is definitive and it is recommended that it is somewhat small (think minutes not hours). |
| 226 | +
|
| 227 | +Compared with traditional sessions, JWTs are quite different. Sessions are always a specific duration from the last interaction with the user. This means that if the user clicks a button, their session is extended. If you think about most applications you use, this is pretty common. You are logged out of the application after a specific amount of inactivity. |
| 228 | +
|
| 229 | +JWTs, on the other hand, are not extended on user interaction. Instead, they are programmatically replaced by creating a new JWT for the user. |
| 230 | +
|
| 231 | +To solve this problem, most applications use refresh tokens. Refresh tokens are opaque tokens that are used to generate new JWTs. Refresh tokens also need to expire at some point, but they can be more flexible in this mechanism because they are persisted in the identity provider or database. |
| 232 | +
|
| 233 | +### CON: Data Overhead |
| 234 | +
|
| 235 | +The overall size of a JWT is quite more than that of a normal session token, which makes it longer whenever more data is added to it. |
| 236 | +
|
| 237 | +So, if you’re adding more claims in the token, it will impact the overall loading speed and thus hamper the user experience. |
| 238 | +
|
| 239 | +This situation can be fixed if the right development practices are followed and minimum but essential data is added to the JWT. |
| 240 | +
|
| 241 | +### CON: JWTs aren’t easily revocable |
| 242 | +
|
| 243 | +This means that a JWT could be valid even though the user’s account has been suspended or deleted. Some solutions around this are available but they mostly require trips to the identity provider or the database, which JWT is essentially developed to minimize. |
| 244 | +
|
| 245 | +So if your app has the potential to deactivate or revoke user access frequently, think twice before using JWTs. |
| 246 | +
|
| 247 | +## Conclusion |
| 248 | +
|
| 249 | +In this lesson, we explored authentication persistence using both server-side sessions and client-side tokens. Each has its pros and cons. Sessions have been used for more than 2 decades now, and they are quite robust. However, their limitation comes from poor portability. |
| 250 | +
|
| 251 | +JWTs, on the other hand, are also robust and pretty portable. However, they come with their overhead and access longevity issues. |
| 252 | +
|
| 253 | +When building your app, always measure the pros and cons of both of these approaches and take your time to decide which one is best for you. |
| 254 | +
|
| 255 | +## Read more |
| 256 | +
|
| 257 | +- https://www.hebergementwebs.com/news/use-of-session-cookies-vs-jwt-for-authentication |
| 258 | +- https://supertokens.io/blog/all-you-need-to-know-about-user-session-security |
| 259 | +- https://supertokens.io/blog/the-best-way-to-securely-manage-user-sessions?s=y |
| 260 | +- https://supertokens.io/blog/are-you-using-jwts-for-user-sessions-in-the-correct-way |
| 261 | +- https://curity.io/resources/learn/phantom-token-pattern/ |
| 262 | +- https://serengetitech.com/tech/what-to-consider-before-using-jwt/ |
| 263 | +
|
| 264 | +## References |
| 265 | +
|
| 266 | +- https://fusionauth.io/learn/expert-advice/tokens/pros-and-cons-of-jwts/ |
| 267 | +- https://www.loginradius.com/blog/start-with-identity/pros-cons-token-authentication/ |
| 268 | +- https://news.ycombinator.com/item?id=22354534 |
0 commit comments