Leia em portugês: clique aqui
You have decided to break your old and outdated monolith application into a more modern and efficient microservices architecture. One of the first problems that will arise comes from the fact that a distributed architecture means your small pieces aren't tied to each other anymore, they will have to talk to each other through a pattern of communication called REST, and no longer they will share in-memory data among them. Well, it just happens that one of the most important pieces of data for your whole application is kept in-memory and shared throughout all modules of your application: the user session.
When you do a little research on microservices authentication and authorization, one technology comes up as the best, if not the only, solution: JWT. Basically, this approach suggests that you put all of your session data into a signed / encrypted hash (the token) and send it back to the client that has logged into your application. So, with every request, the client will send back that token (usually in the request header), and then you can verify the authenticity of the token and extract the session data from it. Session data in hands, you can send it to any service you need until you fulfill that request. You can set up a serverless function to decrypt and verify the signature of the token, or even delegate this task to you API Gateway.
This looks so neat and elegant at first, but wait a minute ... it looks fairly more complex than what it used to be, right? Let's take a look at how it used to work with our monolith application, and why did it have to change so drastically.
For quite a while, HTTP user sessions have been stored in the server's memory, indexed by a randomly generated hash with no meaning - the term "Opaque Token" has even arisen to identify a token with no data in it. This data-less token is then sent back to the browser, and then the server gently asks the browser to store it in a Cookie.
By nature, Cookies are automatically sent back to the server with every request, so after you are logged in, your next request to the server will certainly contain the Cookie, which in turn will contain the token needed to retrieve the respective user data from the server's memory.
This approach has been used for more than a decade by many enterprise-grade application servers, and is considered to be secure. But now we have microservices, so we can't rely on this anymore. Every service is isolated from each other, so we don't have a common place to store this data.
The solution then, as proposed, is to send this data to the client, let it keep the data and send it back whenever needed. This poses a huge bunch of issues we didn't have before, and I'll try to describe some of them now:
Most of the implementations suggest that you send the token back to the server using an HTTP header called "Authorization". While doing so, your client has to have the ability to receive, store and retrieve the token. Problem is, if your client has access to this data, then any malicious code also has. In possession of the token, any attacker can try to decrypt it in order to access the data within it, or just use it to access the application.
In order to keep everything safe, you have to sign the token by running an encryption algorithm so no one will tamper with the data, and also encrypt it to make sure no one will be able to read it.
Also, with every request received, your servers will have to run an unencryption as well as verify the signature by running another encryption. We all know how encryption algorithms are expensive in terms of computing, and none of this encryption / unencryption had to happen with our old monolith, saved by the single run when you have to compare the passwords for login - which you also have to run when using JWT.
JWTs are not the best on tracking session expiration by inactivity. Once you issue a token, it is valid until its own expiration, which is set inside the token. So, either you issue a new token with every request, or you issue another token called Refresh Token with a longer expiration and use it only to get a new token after your token expires. As you may have realized, this just places the same problem elsewhere - the session will expire as soon as the refresh token expires, unless you also refresh it.
As you can see, the solution proposed brings with it a lot of unsolved problems. But how can we achieve effective and secure user session management in a microservices architecture ?
Remember the actual problem: the user session used to be stored in the server's memory, and many enterprise servers could replicate this chunk of memory among all of its instances in a cluster, so it would be accessible no matter what. But now we hardly have enterprise servers anymore, being that many microservice modules are standalone java / node / python / go / (insert your tech here) applications. How can they share a single portion of memory?
It's actually rather simple: add a central session server.
The idea here is to keep session data in the same fashion as before: create a opaque token to use as key, and then you can add as much data as you want to be indexed by that key. You do it in a place that is central and accessible by every microservice on your network, so whenever any of them needs the data, such data is just a call away.
The best tool for this job is Redis. Redis is an in-memory key-value database with sub-milissecond latency. Your microservices can read user session data as if it was stored directly within their own memory (well, almost, but it is fast). Also, Redis has a feature that is key to this application: it can set a timeout to a key-value pair, so as soon as the time expires, the pair is deleted from the database. The timeout can be reset by issuing a command. Sounds exactly like session timeout, right?
In order for this to work, you will need two things:
You will have to create one microservice responsible for the authentication of your users. It will receive the request with the username and password, check if the password is correct, and then create the session data on Redis.
It will have to generate the opaque token, retrieve the user data from your database, and then store the token and the data on Redis. Also, as soon as this is done, it will have to return the token to the client who requested the login, preferably in a SetCookie header if the client is a web browser.
I prefer to make this module sit within every microservice, but you can also set this up on your API Gateway if you like. Its responsibility is to fetch the request and extract the opaque token from it, then reach Redis to retrieve the user session data and make it available to the module that will process that request.
As you can see, the solution is much simpler, faster and more secure than using JWT for user session control. But have in mind these:
- If you are using a single shard of Redis, this can be your single point of failure. I recommend using a more robust production setup with more shards and data replication.
- Session data can be modified by every module with access to Redis - use an "only add never delete" approach, just as you would in the old days.
I hope this helps.
As a bonus, here is a SessionManager to help with the implementation in Java using Jedis and Tomcat's token generator, usually included in Spring Boot: