📜 ⬆️ ⬇️

We authenticate requests in the microservice application using nginx and JWT

Trying to stay in trend and following the trends of web development, I decided to implement the latest web application as a set of microservices on ruby ​​plus a “fat” client on ember. One of the first problems that confronted me was the authentication of requests. If in a classic, monolithic, application, everything is simple, we use cookies, sessions, we connect some devise, then everything is like the first time.

Architecture


For the base, I chose JWT - Json Web Token. This is an open standard RFC 7519 for claims (claims) between two participants. It is a structure of the form: Header.Payload.Signature, where the header and payload are packed hashes in base64 json. Here it is worth paying attention to payload. It can contain anything, in principle, it can be just a client_id and some other information about the user, but this is not a good idea, it’s better to transfer only the key identifier, and store the data somewhere else . Anything can be used as a data warehouse, but it seemed to me that redis would be optimal, especially since it is useful for other tasks. Another important point is how we will sign our token with a key. The easiest option is to use one shared key, but this is clearly not the safest option. Since we store session data in redis, nothing prevents us from generating a unique key for each token and storing it in the same place.

It is clear that the service responsible for authorization will generate tokens, but who and how will check them? In principle, you can shove a check into every microservice, but this contradicts the idea of ​​their maximum separation. Each service will have to contain the logic of processing and checking tokens and also have access to redis. No, our goal is to get an architecture in which all requests coming to the final services are already authorized and carry user data (for example, in some special header).

Checking JWT tokens in NGinx


Here we come to the main part of this article. We need some kind of intermediate element through which all requests would pass and he would authenticate them, fill them with client data and send further. Ideally, the service should be lightweight and easy to scale. The obvious solution would be NGinx reverse proxy, since we can add authentication logic using lua scripts. To be precise, we will use OpenResty - the nginx distribution with a bunch of “buns” out of the box. For greater beauty, we realize all this in the form of a Docker container.
')
I had to start completely from scratch. There is a great project lua-resty-jwt already implementing JWT signature verification. There is even an example of working with redis with a cache for storing a signature, it remains only to finish it in order:

  1. pull a token from the Authorization header
  2. in case of successful verification, retrieve session data and send it in the X-Data header
  3. comb a bit of error to give a valid JSON

The result of the work can be found here: resty-lua-jwt

In nginx.conf, you need to register in the http section a link to the lua package:

http { ... lua_package_path "/lua-resty-jwt/lib/?.lua;;"; lua_shared_dict jwt_key_dict 10m; ... } 

Now in order to authenticate, the request remains in the location section of the add:

 location ~ ^/api/(.*)$ { set $redhost "redis"; set $redport 6379; access_by_lua_file /lua-resty-jwt/jwt.lua; proxy_pass http://upstream/api/$1; } 

We start all this business:

 docker run --name redis redis docker run --link redis -v nginx.conf:/usr/nginx/conf/nginx.conf svyatogor/resty-lua-jwt 

And ready ... well, almost. We must also put the session in the redis and give the client its token. The jwt.lua plugin expects the token in its Payload section to contain a hash via {kid: SESSION_ID}. In redis, this SESSION_ID must match a hash with at least one secret key, which contains the common key for signature verification. There may also be a data key if it finds its contents will go to the upstream service in the X-Data header. In this key we add the user’s serialized object, or, at least, its ID, so that the upstream service understands who the request came from.

Login and token generation


To generate JWT there is a great variety of libraries, a full description is here: jwt.io In my case, I chose jwt gem. Here’s what the action SessionController # create looks like

 def new user = User.find_by_email params[:email] if user && user.authenticate(params[:password]) if user.kid and REDIS.exists(user.kid) > 0 REDIS.del user.kid end key = SecureRandom.base64(24) secret = SecureRandom.base64(24) REDIS.hset key, 'secret', secret REDIS.hset key, 'data', {user_id: user.id}.to_json payload = {"kid" => key} token = JWT.encode payload, secret, 'HS256' render json: {token: token} else render json: {error: "Invalid username or password"}, status: 401 end end 

Now in our UI (ember, angular or mobile app) you need to get a token from the authorization of the service and send it in all requests in the Authorization header. How exactly you will do it depends on your particular case, so I will give only an example with cUrl.

 $ curl -X POST http://default/auth/login -d 'email=user@mail.com' -d 'password=user' {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8YgxyFw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo"}% $ curl http://default/clients/v1/clients -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8Ygxy Fw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo' {"clients":[]} / v1 / clients -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8Ygxy $ curl -X POST http://default/auth/login -d 'email=user@mail.com' -d 'password=user' {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8YgxyFw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo"}% $ curl http://default/clients/v1/clients -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8Ygxy Fw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo' {"clients":[]} 

Afterword


It would be logical to ask whether there are ready-made solutions? I found only Kong from Mashape. For someone else it will be a good variant, since in addition to different types of authorization, he knows how to work with ACLs, manage the load, apply ACLs and much more. In my case, it would have been a cannon shooting at sparrows. In addition, it depends on the Casandra database, which, to put it mildly, is also rather alien to this project.

PPS Imperceptibly "good people" leaked karma. So the plus sign will be very useful and will be a good motivation for writing new articles on the subject of microservices in web development.

Source: https://habr.com/ru/post/277677/


All Articles