Authenticate requests in a microservice application using nginx and JWT
Architecture
For the base, I chose JWT- Json Web Token. This is the open RFC 7519 standard for the submission of claims between two parties. It is a structure of the form: Header.Payload.Signature, where the header and payload are base64-packed json hashes. Payload is worth a look here. It can contain anything you want, in principle it can be just client_id and some other information about the user, but this is not a good idea, it’s better to transfer only the identifier key there, and store the data itself somewhere else . Anything can be used as a data warehouse, but it seemed to me that redis would be optimal, especially since it would be useful for other tasks. Another important point is with what key we will sign our token. The easiest option is to use one shared key, but this is clearly not the safest option.
It is clear that the service responsible for authorization will generate tokens, but who will check them and how? In principle, you can push the check into each microservice, but this contradicts the idea of their maximum separation. Each service will have to contain the logic of processing and checking tokens and even have access to redis. No, our goal is to get an architecture in which all the requests coming to the final services are already authorized and carry the data about the user (for example, in some special header).
Checking JWT Tokens in NGinx
Here we come to the main part of this article. We need some sort of intermediate element through which all requests would go and he would authenticate them, fill them with client data and send them on. Ideally, the service should be lightweight and easy to scale. The obvious solution would be NGinx reverse proxy, since we can add authentication logic to it using lua scripts. To be precise, we will use OpenResty - the nginx distribution with a bunch of “goodies” out of the box. For greater beauty, we implement all this in the form of a Docker container.
I had to start completely from scratch. There is a wonderful lua-resty-jwt project that already implements JWT signature verification. There is even an example of working with redis cache for signature storage, it remains only to finish it so that:
- pull token from Authorization header
- in case of successful verification, get session data and send it in the X-Data header
- comb the errors a bit to get valid JSON
The result of the work can be found here: resty-lua-jwt
In nginx.conf, you need to register a link to the lua package in the http section:
http {
...
lua_package_path "/lua-resty-jwt/lib/?.lua;;";
lua_shared_dict jwt_key_dict 10m;
...
}Now, in order to authenticate the request, it remains to press the location section:
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 this whole thing:
docker run --name redis redis
docker run --link redis -v nginx.conf:/usr/nginx/conf/nginx.conf svyatogor/resty-lua-jwtAnd you're done ... well, almost. We must also put a session in redis and give the client its token. The jwt.lua plugin expects the token in its Payload section to contain the hash via {kid: SESSION_ID}. In redis, a hash must correspond to this SESSION_ID with at least one secret key, in which there is a public key to verify the signature. There may also be a data key, if it finds then its contents will go to the upstream service in the X-Data header. In this key, we add the serialized user object, or at least its ID, so that the upstream service understands who the request came from.
Login and Token Generation
There are a great many libraries for generating JWT, a full description is here: jwt.io In my case, I chose the jwt gem. Here's what 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
endNow in our UI (ember, angular or mobile application) you need to get a token from the authorization service and pass it in all requests in the Authorization header. How exactly you will do this depends on your specific case, so I will give only an example with cUrl.
$ curl -X POST http://default/auth/login -d '[email protected]' -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 if there are any ready-made solutions? I found only Kong from Mashape. For someone else, this will be a good variation, because in addition to different types of authorization, he knows how to work with ACLs, manage the load, use ACLs and much more. In my case, it would be shooting from a cannon at sparrows. In addition, it depends on the Casandra database, which, to put it mildly, is tazhelovat yes and quite alien to this project.
PPS Silently "kind people" leaked karma. So the plus sign will be very helpful and will be a good motivation for writing new articles on microservices in web development.