I was trying to create my first actual microservice program and very soon I faced an issue: "How many times I should do the authentication?"
In the first part, I describe the situation and in the second part, I show how should we configure the express gateway to perform the jwt authentication and pass the claims in the request to the service endpoints. Feel free to skip the first part and go straight to the second part. And if you only want the express-gateway jwt part, go to Adding the authenticated data to the request at the end of the article, there is a complete config which could be used.
I was going to use jwt token authentication for users. I had 2 microservices, one for user authentication (sign up, sign in, sign out) and one for CURD operations on a model, we call them auth-service and curd-service. I started writing code, and developed the auth-service, as you know jwt uses a key to deal with the tokens (create them or verify them). Everything worked fine until I started to develop the curd-service. I needed to authenticate the user in the request before performing the desired curd-service operation, but how?
When the request (let say GET
/books/:_id)
entered the system, it didn't necessarily go through the auth-service, and it probably contains the jwt token in the header. If I wanted to verify and decrypt the token in the crud-service I needed the secure-key in the crud-service. It instantly raised the question of whether I should put the secure-key in the curd-service or not? What if there are many services which may be at the front line of a system, do all of them need to have access to the "secure"-key? I started to look for a solution, I wasn't the first person who had more than one service in his system. I found very good articles and learned a lot.Long story short, one of the solutions is to have an API gateway, so every request goes through it. Adding an API gateway to your architecture has many benefits, it can perform rate limiting, request logging, acting as a proxy, handing CORS and also authenticating the requests. Although like anything else in life, it comes with its drawbacks, it can become a bottleneck or a single point of failure in the whole system. Anyway, I decided to use an API gateway as seemed (and still does) like a very good solution. By using API gateway, we can perform the authentication at the API gateway and only put the secret-key at the API gateway (besides the auth-service itself).
I started looking for an API gateway and decided to go with Express-Gateway.
The documentation on the Express Gateway is pretty neat and you can find almost anything where it should be. Here we are going to configure the express gateway to add the decoded authentication data ( jwt claims) to the request before passing it to our other services.
As is mentioned in the express gateway docs, it supports different ways for handling the jwt token in the request. Here I use the default way which is having
Authorization
header with Bearer
keyword. 1. Starting Point
To be on the same page, let's say we have an auth-service which performs login and register under
/auth/register
and /auth/login
on the host auth:3003
. We also have a curd-service for books which all start with /books
and they are served at books:4004
. We need the api gateway to handle different requests.
/auth/register
and /auth/login
need to be logged, and passed to auth-service
as these request are not authenticated and thats the reason they are talking to auth-service
. But for requests matching /books*
the gateway needs to make sure they are authenticated (contain a valid token) and also decrypt the token and add its content to the request before handing the request to curd-service
.2. Create a new express gateway
This is the same as it is mentioned on docs.
a. Install Express Gateway
npm install -g express-gateway
b. Create an Express Gateway
$ eg gateway create
c. Answer a few questions
➜ eg gateway create
? What is the name of your Express Gateway? my-gateway
? Where would you like to install your Express Gateway? my-gateway
? What type of Express Gateway do you want to create? (Use arrow keys)
❯ Getting Started with Express Gateway
Basic (default pipeline with proxy)
d. For running it, go to the folder and run
npm start
3. Configure the gateway to log and proxy the requests
After creating a new express gateway it generates a pretty complete and runnable template for us which only needs to be configured. All of the configuration we need to do are at the
/config/gateway.config.yml
. It also supports hot-reloading, so you don't need to restart the express gateway every time you change something in the config files. I'm not going into details of how to configure express gateway because as I mentioned before the documentation is fine. I just mention the core concepts and also suggest that you read the docs. There are a few core concepts at express gateway, apiEndpoints represent the endpoint of incoming requests (the ones system's users are sending,
/auth/register
, /auth/login
and /books*
in our example). serviceEndpoints represent where the requests should be redirected to (auth:3003
, books:4004
in our example). pipelines are actually telling the express gateway how to handle the requests. principles tell the express gateway which tools we are going to use in the pipelines section (logging, authentication and proxy in our example).We first define the log and proxy for
/auth*
Here we are telling the express gateway to listen to port 9080 for incoming requests, if they match the pattern
/auth*
and their HTTP method is POST, log them and redirect them to http://auth:3003
. Please note there is no authentication happening here.4. Authenticate and adding the authenticated data to the request
If the requests contain a token which is generated using a secret-key, express gateway can authenticate and decrypt the token (assuming we provide it the same key) and put the authenticated data in the
req.user
. We further need to add req.user
to the request before passing it to the desired serviceEndpoint.http:
port: 9080
admin:
port: 9876
host: localhost
apiEndpoints:
auth:
path: '/auth*'
methods: ['POST']
serviceEndpoints:
auth:
url: 'http://auth:3003'
policies:
- log
- proxy
pipelines:
authPipeline:
apiEndpoints:
- auth
policies:
-
log:
action:
message: 'auth ${req.method}'
-
proxy:
action:
serviceEndpoint: auth
Please notice that you need to update the policies section too. And just adding the new serviceEndpoint, apiEndpoint and pipeline is not enough.
http:
port: 9080
admin:
port: 9876
host: localhost
apiEndpoints:
auth:
path: '/auth*'
methods: ['POST']
books:
path: '/books*'
serviceEndpoints:
auth:
url: 'http://auth:3003'
books:
url: 'http://books:4004'
policies:
- log
- proxy
- jwt
- request-transformer
pipelines:
authPipeline:
apiEndpoints:
- auth
policies:
-
log:
action:
message: 'auth ${req.method}'
-
proxy:
action:
serviceEndpoint: auth
booksPipeline:
apiEndpoints:
- books
policies:
-
log:
action:
message: 'books ${req.method}'
-
jwt:
action:
secretOrPublicKey: 'the-secret-key'
checkCredentialExistence: false
-
request-transformer:
action:
body:
add:
user: req.user
-
proxy:
action:
serviceEndpoint: books
the new pipeline (booksPipeline) is performing the proxying and logging action in the same way authEndpoint performs it. The difference is that we are using two new policies.
jwt
and request-transformer
.JWT
jwt
is in charge of authenticating the jwt token in the header, by default it tries to locate it in the Authorization
header but it's possible to define other approaches using the jwtExtractor
and jwtExtractorField
properties. If express gateway doesn't find a valid token it responds to the request with a 401 error, which is great as the request won't enter the system and there is 1 less request curd-service
needs to be worry about. secretOrPublicKey
specifies the secret-key, we could also use the secretOrPublicKeyFile
. By setting the
checkCredentialExistence
as false
we are telling the express gateway we want it in the Uncontrolled modality mode, you can read about this here. Briefly, it is saying that we create and manage the tokens in another service (express gateway has the option to create the tokens through its admin API with Controlled Modality)request-transformer
As the name suggests
request-transformer
transforms the request by adding or removing different values to its header or body. If you don't add this to your pipeline, the decoded information from the token won't be available to the serviceEndpoint in which express gateway redirects the requests. This is a intentional and a secure behavior, we don't want anything to be added to or removed from our request without us knowing.5. What comes next?
Now, when the request enters the
curd-service
we are sure it's authenticated and it contains the decoded information of the token (user id, role, ....) in the req.user
. You would also have the jwt token unchanged, if you wish to remove it from the request for any reason (maybe security), you can do it by another request-transformer
policy which can also remove stuff.
I hope this has been useful, I'd be glad to get any feedback.