Authentication for Microservices
What is Authentication?
User authentication is the process of verifying that a user is who they actually claim to be. When providing a username on a website like Facebook, it is also necessary to provide a password to verify that the actual owner of that account is trying to access it.
Recently, there has been an increase in multi-factor authentication, which is an added measure of security used for applications that contain much more sensitive data. These other “factors” simply mean other methods of authentication other than a password and are most commonly carried out by sending verification codes to a registered mobile phone or email address.
A further step used by applications where security is of paramount importance is the need to login every few minutes or hours if there has been no activity (user has remained idle).
In modern web applications, authentication is handled by using cookies, sessions and tokens and these may be stored on the client-side or server-side according to the implementation and requirements.
What is Authorization?
Authorization on the other hand, is the process of verifying that a certain user has the required access right to a certain resource. In an e-commerce web application, only certain users should be allowed to add new products to the database and different applications can have various levels of access permission according to their requirements.
Authentication in Microservices
When designing an application using a microservice based architecture, the method of implementing security is a bit more complicated. This is because there are several independent services running simultaneously which all need to implement authentication without depending on other services.
one approach: central auth server; analyze and reject
auth flow: send request along with cookie/in request header
request handlers in every service should check whether user is logged in
A Common Solution: Auth-Service
The most common approach to handling authentication when using microservices is to use a central auth-service. Individual services will rely on this as a utility service for handling all aspects of authentication and authorization. This can be implemented in one of the following ways:
- Every incoming request will be first handled by the auth-service which will then forward it to the appropriate service if successfully authenticated.
- Individual services will send a request to the auth-service whenever any security checks are required.
Drawbacks
- The biggest disadvantage is that if the auth-service fails for any reason, the whole application will come to a standstill.
- The auth-service could very easily be overwhelmed by requests coming from every service and may result in being a bottleneck to the application.
Another Solution: Independent implementation
Another method of implementing authentication is to define all security rules and logic in each and every service independently. This would easily solve the drawbacks of the previous solution and would be a more suitable approach to take when working with microservices. The idea of completely independent services is a crucial aspect in such an architecture and in my opinion is the way to go. However, it is not without drawbacks either:
Drawbacks
- This approach would lead to a fair amount of code duplication since the same logic needs to be repeated for each service. We can reduce this duplication by placing common logic in a common module and importing it into each service.
- If there is a change to any user authentication data made on one service, the other services will not know about it since they are all independent. ex: A user may be banned by by a user-service but a booking-service would not know.
Cookies and JWTs
What is a Cookie?
When a browser makes a request to a server, the server will respond with some data. When sending this response it is possible to include a header containing a cookie using a Set-Cookie header. This cookie is a simple text file containing some data enabling a website to identify a specific user in order to improve their browsing experience.
The browser will store this cookie locally and include it in any subsequent request it makes to the same domain as a request header. This enables the transfer of user data between the client and server, allowing the server to keep track of a user session.
What is a JSON Web Token?
A JWT is simply a string of encoded characters used as a token as in the example below:
It is used as an authorization mechanism and consists of three parts: a header, payload and a signature. The payload contains the data being transmitted and the signature signs the two other components according to a specified algorithm with the use of a secret key. A JWT is therefore completely tamper-resistant and cannot be altered.
A JWT is usually communicated between the client and the server as a bearer token via the Authorization header.
However, this leads to a dilemma when using server side rendering to serve content. When using traditional client side rendering, the client will make several requests to the server (once for the initial HTML content, again for the JS bundle and then for the actual resource) to view some content. However, it is now widely accepted that using SSR provides a faster and more efficient experience for the consumer since the front end view logic is executed on the server instead of on the client-side. This allows devices with different capacities and speeds to render the same content at identical speeds as there is no need to do any actual rendering on the device itself.
With SSR, we need only send one request to the server to access a certain resource and the problem arises when deciding what method to use to send a JWT to the server with this initial request. It is usually not possible (unless we use service workers) to place the token onto an Authorization header or to the request body when making an initial request to a server and we need to therefore use a cookie.
We can simply place the token inside a cookie and use that as the transport mechanism. Using cookies to transport JWTs will however require CORS to be enabled.
our auth mechanism needs to have the following:
- info about the user
- is user authenticated
- include auth info (role)
- should have a way of expiring (tamper-resistant)
Implementation of Cookie based Authentication
I prefer to use the npm module cookie-session to implement a cookie based mechanism for storing and communicating JWTs. The reason for using this particular library is because it stores session data on the client (browser) within the cookie itself. This is contrary to the usual way of storing session data on a server-side database with only the session ID stored on the cookie.
- In a microservice based approach, it is advisable to assume that different services may be written using different languages, therefore by storing session data on the client side, we don’t need to have this logic on the server-side. Using this approach will be helpful when adding more services in the future.
Furthermore, it is not essential to encrypt the cookie containing the JWT since JWTs are naturally tamper-resistant.
Shown below is the way we apply this library to our authorization service.
Sign Up Logic
For creating and verifying JWTs, we can use the jsonwebtoken npm package. After a user account is created and saved to the database through the signup flow, we can generate a JWT and store it on the session object.
The first argument to to sign() is the user data that needs to be stored in the token and as a second argument we pass a secret key. When making a synchronous call as shown above the token is returned immediately as a string.
Sign In Logic
When the client makes a sign-in request, the server first checks whether a user with the provided email/username exists in the user database. If that is true, the provided password is compared with stored password. If the passwords match the server responds with a cookie containing a JWT.
Current User Logic
When a user makes a request to access a protected resource, the client needs to check whether that user is authenticated i.e. logged in. A request coming from an authenticated user will contain a cookie with a valid JWT and to check this we can first call a helper API as shown below:
If the user is logged in, a JWT will be stored in the req.session.jwt property.
The next step is to decode the token to confirm its validity and extract its payload using verify(). We pass the token and the secret key to this function and wrap it inside a try/catch block. This is an additional precaution since an invalid token will throw an error.
If the token is present and valid, we send the payload (user object) back as a response. This will be used by the client to decide if a user should be granted access to a certain resource.
*The above code should ideally be refactored as a middleware function.
Sign Out Logic
To terminate a user session using cookie-session we destroy the session object by setting the req.session property to null:
This will effectively instruct the browser to delete the cookie stored in the local storage requiring the user to log in again.