The goal is to create a Spring Boot application to manage books, called book-service and secure it by using Kong API gateway and Keycloak OpenID Connect Provider.
Note: In
kubernetes-minikube-environmentrepository, it's shown how to deploy this project inKubernetes(Minikube)
On ivangfr.github.io, I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for.
- [Medium] Using Kong to secure a Simple Spring Boot REST API with Kong OIDC plugin and Keycloak
- [Medium] Using Kong to secure a Simple Spring Boot REST API with Basic Authentication plugin
- [Medium] Using Kong to secure a Simple Spring Boot REST API with LDAP Authentication plugin
- [Medium] Using Kong to configure Rate Limiting to a Simple Spring Boot REST API
- [Medium] Implementing and Securing a Simple Spring Boot REST API with Keycloak
As we can see from the diagram, book-service will only be reachable through Kong API gateway.
In Kong, it's installed kong-oidc plugin that will enable the communication between Kong and Keycloak OpenID Connect Provider.
This way, when Kong receives a request to book-service, it will validate with Keycloak whether it's a valid request.
Also, before redirecting to the request to the upstream service, a Serverless Function (post-function) will get the access token present in the X-Userinfo header provided by kong-oidc plugin, decode it, extract the username and preferred_username, and enrich the request with these two information before sending to book-service
-
Spring BootREST API application to manage books. The API doesn't have any security.book-serviceusesMongoDBas its storage.Endpoints:
GET /actuator/health GET /api/books POST /api/books {"isbn": "...", "title": "..."} GET /api/books/{isbn} DELETE /api/books/{isbn}
-
Open a terminal and navigate to the
springboot-kong-keycloakroot folder. -
Run the command below to start
mongodbDocker container:docker run -d --name mongodb -p 27017:27017 mongo:8.0.3
-
Run the command below to start
book-service:./mvnw clean spring-boot:run --projects book-service
-
Open another terminal and call application endpoints:
curl -i localhost:9080/api/books curl -i -X POST localhost:9080/api/books -H "Content-Type: application/json" -d '{"isbn":"123", "title":"Kong & Keycloak"}' curl -i localhost:9080/api/books/123 curl -i -X DELETE localhost:9080/api/books/123 curl -i localhost:9080/actuator/health
-
To stop:
book-service, go to the terminal where it's running and pressCtrl+C.mongodbDocker container, go to a terminal and run the following command:docker rm -fv mongodb
-
In a terminal, make sure you are in the
springboot-kong-keycloakroot folder. -
Build Docker Image
- JVM
./build-docker-images.sh
- Native
./build-docker-images.sh native
Environment Variable Description MONGODB_HOSTSpecify host of the Mongodatabase to use (defaultlocalhost)MONGODB_PORTSpecify port of the Mongodatabase to use (default27017) - JVM
-
In a terminal, create a Docker network:
docker network create springboot-kong-keycloak-net
-
Run the command below to start
mongodbDocker container:docker run -d --name mongodb -p 27017:27017 --network springboot-kong-keycloak-net mongo:8.0.3
-
Run the following command to start
book-serviceDocker container:docker run --rm -p 9080:9080 --name book-service -e MONGODB_HOST=mongodb --network springboot-kong-keycloak-net ivanfranchin/book-service:1.0.0
-
Open another terminal and call application endpoints:
curl -i localhost:9080/api/books curl -i -X POST localhost:9080/api/books -H "Content-Type: application/json" -d '{"isbn":"123", "title":"Kong & Keycloak"}' curl -i localhost:9080/api/books/123 curl -i -X DELETE localhost:9080/api/books/123 curl -i localhost:9080/actuator/health
-
To stop:
book-service, go to the terminal where it's running and pressCtrl+C.mongodbDocker container, go to a terminal and run the following command:docker rm -fv mongodb
- Remove Docker network:
docker network rm springboot-kong-keycloak-net
-
In a terminal, make use you are in the
springboot-kong-keycloakroot folder. -
Run the following script:
./init-environment.sh
Note:
book-serviceapplication is running as a Docker container. The container does not expose any port to HOST machine. So, it cannot be accessed directly, forcing the caller to useKongas gateway server in order to access it.
-
In a terminal, make sure you are in the
springboot-kong-keycloakroot folder. -
Run the following script to configure
Keycloakforbook-serviceapplication:./init-keycloak.sh
This script creates:
company-servicesrealm;book-serviceclient;- user with username
ivan.franchinand password123.
-
The
book-serviceclient secret (BOOK_SERVICE_CLIENT_SECRET) is shown at the end of the execution. It will be used in the next step. -
You can check the configuration in
Keycloakby accessing http://localhost:8080. The credentials areadmin/admin.
-
In a terminal, make sure you are in the
springboot-kong-keycloakroot folder. -
Create an environment variable that contains the
Client Secretgenerated byKeycloaktobook-serviceat Configure Keycloak step:BOOK_SERVICE_CLIENT_SECRET=...
-
Run the following script to configure
Kongforbook-serviceapplication:./init-kong.sh $BOOK_SERVICE_CLIENT_SECRETThis script creates:
- service to
book-service; - route to
/actuatorpath; - route to
/apipath; - add
kong-oidcplugin to route of/apipath. It will authenticate users againstKeycloakOpenID Connect Provider; - add
serverless function (post-function)plugin to route of/apipath. It gets the access token present in theX-Userinfoheader provided by thekong-oidcplugin, decodes it, extracts theusernameandpreferred_username, and enriches the request with this information before sending it tobook-service.
- service to
-
Try to call the public
GET /actuator/healthendpoint:curl -i localhost:8000/actuator/health -H 'Host: book-service'It should return:
HTTP/1.1 200 {"status":"UP"} -
Try to call the private
GET /api/booksendpoint without access token:curl -i localhost:8000/api/books -H 'Host: book-service'It should return:
HTTP/1.1 401 Unauthorized no Authorization header found -
Get
ivan.franchinaccess token:ACCESS_TOKEN=$(./get-access-token.sh $BOOK_SERVICE_CLIENT_SECRET) && echo $ACCESS_TOKEN
Note: In
jwt.io, you can decode and verify theJWTaccess token -
Call again the private
GET /api/booksendpoint using the access token:curl -i localhost:8000/api/books \ -H 'Host: book-service' \ -H "Authorization: Bearer $ACCESS_TOKEN"
It should return:
HTTP/1.1 200 [] -
You can try other endpoints using access token:
Create book
curl -i -X POST localhost:8000/api/books \ -H 'Host: book-service' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" -d '{"isbn": "123", "title": "Kong & Keycloak"}'
Get book
curl -i localhost:8000/api/books/123 \ -H 'Host: book-service' \ -H "Authorization: Bearer $ACCESS_TOKEN"
Delete book
curl -i -X DELETE localhost:8000/api/books/123 \ -H 'Host: book-service' \ -H "Authorization: Bearer $ACCESS_TOKEN"
-
MongoDB
List books
docker exec -it mongodb mongosh bookdb db.books.find()
Type
exitto get out of MongoDB shell
In a terminal and, inside the springboot-kong-keycloak root folder, run the following script:
./shutdown-environment.shTo remove the Docker image created by this project, in a terminal and, inside the springboot-kong-keycloak root folder, run the script below:
./remove-docker-images.sh-
Unable to upgrade to
kongversion3.xbecauseBasePluginclass was deprecated inkongversion2.4.xand removed in version3.0.xlink. Now,kong-oidcneeds to supportkongversion3.xissue; -
When upgrading
postgresto a version above13.x(using current kong version), there is an error while runningkong[error] 1#0: init_by_lua error: /usr/local/share/lua/5.1/pgmoon/init.lua:273: module 'openssl.rand' not found:No LuaRocks module found for openssl.rand no field package.preload['openssl.rand'] no file './openssl/rand.lua' no file './openssl/rand/init.lua' no file '/usr/local/openresty/site/lualib/openssl/rand.ljbc' no file '/usr/local/openresty/site/lualib/openssl/rand/init.ljbc' no file '/usr/local/openresty/lualib/openssl/rand.ljbc' no file '/usr/local/openresty/lualib/openssl/rand/init.ljbc' no file '/usr/local/openresty/site/lualib/openssl/rand.lua' no file '/usr/local/openresty/site/lualib/openssl/rand/init.lua' no file '/usr/local/openresty/lualib/openssl/rand.lua' no file '/usr/local/openresty/lualib/openssl/rand/init.lua' no file '/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/openssl/rand.lua' no file '/usr/local/share/lua/5.1/openssl/rand.lua' no file '/usr/local/share/lua/5.1/openssl/rand/init.lua' no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand.lua' no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand/init.lua' no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand.lua' no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand/init.lua' no file '/usr/local/openresty/site/lualib/openssl/rand.so' no file '/usr/local/openresty/lualib/openssl/rand.so' no file './openssl/rand.so' no file '/usr/local/lib/lua/5.1/openssl/rand.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl/rand.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file '/home/kong/.luarocks/lib/lua/5.1/openssl/rand.so' no file '/usr/local/openresty/site/lualib/openssl.so' no file '/usr/local/openresty/lualib/openssl.so' no file './openssl.so' no file '/usr/local/lib/lua/5.1/openssl.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file '/home/kong/.luarocks/lib/lua/5.1/openssl.so' stack traceback: [C]: in function 'require' /usr/local/share/lua/5.1/pgmoon/init.lua:273: in function 'auth' /usr/local/share/lua/5.1/pgmoon/init.lua:213: in function 'connect' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:216: in function 'connect' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:516: in function 'query' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:284: in function 'init' /usr/local/share/lua/5.1/kong/db/init.lua:141: in function 'init_connector' /usr/local/share/lua/5.1/kong/init.lua:503: in function 'init' init_by_lua:3: in main chunk nginx: [error] init_by_lua error: /usr/local/share/lua/5.1/pgmoon/init.lua:273: module 'openssl.rand' not found:No LuaRocks module found for openssl.rand no field package.preload['openssl.rand'] no file './openssl/rand.lua' no file './openssl/rand/init.lua' no file '/usr/local/openresty/site/lualib/openssl/rand.ljbc' no file '/usr/local/openresty/site/lualib/openssl/rand/init.ljbc' no file '/usr/local/openresty/lualib/openssl/rand.ljbc' no file '/usr/local/openresty/lualib/openssl/rand/init.ljbc' no file '/usr/local/openresty/site/lualib/openssl/rand.lua' no file '/usr/local/openresty/site/lualib/openssl/rand/init.lua' no file '/usr/local/openresty/lualib/openssl/rand.lua' no file '/usr/local/openresty/lualib/openssl/rand/init.lua' no file '/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/openssl/rand.lua' no file '/usr/local/share/lua/5.1/openssl/rand.lua' no file '/usr/local/share/lua/5.1/openssl/rand/init.lua' no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand.lua' no file '/usr/local/openresty/luajit/share/lua/5.1/openssl/rand/init.lua' no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand.lua' no file '/home/kong/.luarocks/share/lua/5.1/openssl/rand/init.lua' no file '/usr/local/openresty/site/lualib/openssl/rand.so' no file '/usr/local/openresty/lualib/openssl/rand.so' no file './openssl/rand.so' no file '/usr/local/lib/lua/5.1/openssl/rand.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl/rand.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file '/home/kong/.luarocks/lib/lua/5.1/openssl/rand.so' no file '/usr/local/openresty/site/lualib/openssl.so' no file '/usr/local/openresty/lualib/openssl.so' no file './openssl.so' no file '/usr/local/lib/lua/5.1/openssl.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/openssl.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file '/home/kong/.luarocks/lib/lua/5.1/openssl.so' stack traceback: [C]: in function 'require' /usr/local/share/lua/5.1/pgmoon/init.lua:273: in function 'auth' /usr/local/share/lua/5.1/pgmoon/init.lua:213: in function 'connect' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:216: in function 'connect' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:516: in function 'query' .../share/lua/5.1/kong/db/strategies/postgres/connector.lua:284: in function 'init' /usr/local/share/lua/5.1/kong/db/init.lua:141: in function 'init_connector' /usr/local/share/lua/5.1/kong/init.lua:503: in function 'init' init_by_lua:3: in main chunk
