springboot-keycloak-openldap
The goal of this project is to create a simple Spring Boot REST API, called simple-service
, and secure it with Keycloak
. Furthermore, the API users will be loaded into Keycloak
from OpenLDAP
server.
Note 1: In
springboot-react-keycloak
repository, we have implemented amovies-app
usingKeycloak
(withPKCE
). This application consists of two services: the backend that was implemented usingSpring Boot
and the frontend implemented withReactJS
.
Note 2: In
docker-swarm-environment
repository, it's shown how to deploy this project into a cluster of Docker Engines in swarm mode. Besides, we will be running a Keycloak cluster with more than one instance.
Additional Readings
- [Medium] How To Secure A Simple Spring Boot API With Keycloak
- [Medium] How To Secure A Simple Spring Boot UI (Thymeleaf + RBAC) With Keycloak
- [Medium] Setting Up OpenLDAP With Keycloak For User Federation
- [Medium] Integrating GitHub as a Social Identity Provider in Keycloak
- [Medium] Integrating Google as a Social Identity Provider in Keycloak
Project Diagram
Application
-
simple-service
Spring Boot
Web Java application that exposes the following endpoints:GET /api/public
: it's a not secured endpoint, everybody can access it;GET /api/private
: it's a secured endpoint, only accessible by users that provide aJWT
access token issued byKeycloak
and the token must contain the roleUSER
;GET /actuator/*
: they are not secured endpoint, used to expose operational information about the application.
Prerequisites
Start Environment
-
Open a terminal and inside
springboot-keycloak-openldap
root folder rundocker-compose up -d
-
Wait for Docker containers to be up and running. To check it, run
docker-compose ps
Import OpenLDAP Users
The LDIF
file that we will use, springboot-keycloak-openldap/ldap/ldap-mycompany-com.ldif
, contains a pre-defined structure for mycompany.com
. Basically, it has 2 groups (developers
and admin
) and 4 users (Bill Gates
, Steve Jobs
, Mark Cuban
and Ivan Franchin
). Besides, it's defined that Bill Gates
, Steve Jobs
and Mark Cuban
belong to developers
group and Ivan Franchin
belongs to admin
group.
Bill Gates > username: bgates, password: 123
Steve Jobs > username: sjobs, password: 123
Mark Cuban > username: mcuban, password: 123
Ivan Franchin > username: ifranchin, password: 123
There are two ways to import those users: running a script or using phpldapadmin
website
Running a script
-
In a terminal and inside
springboot-keycloak-openldap
root folder run./import-openldap-users.sh
-
The command below can be used to check the users imported
ldapsearch -x -D "cn=admin,dc=mycompany,dc=com" \ -w admin -H ldap://localhost:389 \ -b "ou=users,dc=mycompany,dc=com" \ -s sub "(uid=*)"
Using phpldapadmin website
-
Access https://localhost:6443
-
Login with the credentials
Login DN: cn=admin,dc=mycompany,dc=com Password: admin
-
Import the file
springboot-keycloak-openldap/ldap/ldap-mycompany-com.ldif
-
You should see a tree like the one shown in the picture below
Configure Keycloak
There are two ways: running a script or using Keycloak
website
Running a script
-
In a terminal, make sure you are inside
springboot-keycloak-openldap
root folder -
Run the script below to configure
Keycloak
forsimple-service
application./init-keycloak.sh
It creates
company-services
realm,simple-service
client,USER
client role,ldap
federation and the usersbgates
andsjobs
with the roleUSER
assigned. -
Copy
SIMPLE_SERVICE_CLIENT_SECRET
value that is shown at the end of the script. It will be needed whenever we callKeycloak
to get aJWT
access token to accesssimple-service
Using Keycloak website
Please, have a look at this Medium article, Setting Up OpenLDAP With Keycloak For User Federation
Run simple-service using Maven
-
Open a new terminal and make sure you are in
springboot-keycloak-openldap
root folder -
Start the application by running the following command
./mvnw clean spring-boot:run --projects simple-service -Dspring-boot.run.jvmArguments="-Dserver.port=9080"
Test using curl
-
Open a new terminal
-
Call the endpoint
GET /api/public
curl -i http://localhost:9080/api/public
It should return
HTTP/1.1 200 It is public.
-
Try to call the endpoint
GET /api/private
without authenticationcurl -i http://localhost:9080/api/private
It should return
HTTP/1.1 401
-
Create an environment variable that contains the
Client Secret
generated byKeycloak
tosimple-service
at Configure Keycloak stepSIMPLE_SERVICE_CLIENT_SECRET=...
-
Run the command below to get an access token for
bgates
userBGATES_ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/company-services/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=bgates" \ -d "password=123" \ -d "grant_type=password" \ -d "client_secret=$SIMPLE_SERVICE_CLIENT_SECRET" \ -d "client_id=simple-service" | jq -r .access_token)
Note: In jwt.io, you can decode and verify the
JWT
access token -
Call the endpoint
GET /api/private
curl -i http://localhost:9080/api/private -H "Authorization: Bearer $BGATES_ACCESS_TOKEN"
It should return
HTTP/1.1 200 bgates, it is private.
-
Run the command below to get an access token for
mcuban
userMCUBAN_ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/company-services/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=mcuban" \ -d "password=123" \ -d "grant_type=password" \ -d "client_secret=$SIMPLE_SERVICE_CLIENT_SECRET" \ -d "client_id=simple-service" | jq -r .access_token)
-
Try to call the endpoint
GET /api/private
curl -i http://localhost:9080/api/private -H "Authorization: Bearer $MCUBAN_ACCESS_TOKEN"
As
mcuban
does not have theUSER
role assigned, he cannot access this endpoint.The endpoint return should be
HTTP/1.1 403 WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
-
Go to
Keycloak
and add the roleUSER
to themcuban
-
Run the command mentioned in
step 7)
again to get a new access token formcuban
user -
Call again the endpoint
GET /api/private
using thecurl
command presented instep 8
It should return
HTTP/1.1 200 mcuban, it is private.
-
The access token default expiration period is
5 minutes
. So, wait for this time and, using the same access token, try to call the private endpoint.It should return
HTTP/1.1 401 WWW-Authenticate: Bearer realm="company-services", error="invalid_token", error_description="Token is not active"
Test using Swagger
-
Click
GET /api/public
to open it. Then, clickTry it out
button and, finally, clickExecute
buttonIt should return
Code: 200 Response Body: It is public.
-
Now click
GET /api/private
secured endpoint. Let's try it without authentication. Then, clickTry it out
button and, finally, clickExecute
buttonIt should return
Code: 401 Details: Error: response status is 401
-
In order to access the private endpoint, you need an access token. So, open a terminal
-
Create an environment variable that contains the
Client Secret
generated byKeycloak
tosimple-service
at Configure Keycloak stepSIMPLE_SERVICE_CLIENT_SECRET=...
-
Run the following commands
BGATES_ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/company-services/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=bgates" \ -d "password=123" \ -d "grant_type=password" \ -d "client_secret=$SIMPLE_SERVICE_CLIENT_SECRET" \ -d "client_id=simple-service" | jq -r .access_token) echo $BGATES_ACCESS_TOKEN
-
Copy the token generated and go back to
Swagger
-
Click
Authorize
button and paste the access token in theValue
field. Then, clickAuthorize
button and, to finalize, clickClose
-
Go to
GET /api/private
and call this endpoint again, now with authenticationIt should return
Code: 200 Response Body: bgates, it is private.
Using client_id and client_secret to get access token
You can get an access token to simple-service
using client_id
and client_secret
Configuration
- Access http://localhost:8080/admin/
- Select
company-services
realm (if it's not already selected) - On the left menu, click
Clients
- Select
simple-service
client - In
Settings
tab- Go to
Capability config
and checkService accounts roles
checkbox - Click
Save
button
- Go to
- In
Service account roles
tab- Click
service-account-simple-service
link present in the info message"To manage detail and group mappings, click on the username service-account-simple-service"
- In
Role mapping
tab- Click
Assign role
button - Click
Filter by realm roles
dropdown button and selectFilter by clients
- In
Search by role name
typesimple-service
and pressEnter
- Select
USER
role of thesimple-service
and clickAssign
button - Now,
service-account-simple-service
has the roleUSER
ofsimple-service
assigned
- Click
- Click
Test
-
Open a terminal
-
Create an environment variable that contains the
Client Secret
generated byKeycloak
tosimple-service
at Configure Keycloak stepSIMPLE_SERVICE_CLIENT_SECRET=...
-
Run the following command
CLIENT_ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/company-services/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_secret=$SIMPLE_SERVICE_CLIENT_SECRET" \ -d "client_id=simple-service" | jq -r .access_token)
-
Try to call the endpoint
GET /api/private
curl -i http://localhost:9080/api/private -H "Authorization: Bearer $CLIENT_ACCESS_TOKEN"
It should return
HTTP/1.1 200 service-account-simple-service, it is private.
Running simple-service as a Docker container
-
In a terminal, make sure you are in
springboot-keycloak-openldap
root folder -
Build Docker Image
- JVM
./docker-build.sh
- Native
./docker-build.sh native
Environment Variable Description KEYCLOAK_HOST
Specify host of the Keycloak
to use (defaultlocalhost
)KEYCLOAK_PORT
Specify port of the Keycloak
to use (default8080
) - JVM
-
Run Docker Container
docker run --rm --name simple-service \ -p 9080:8080 \ -e KEYCLOAK_HOST=keycloak \ --network=springboot-keycloak-openldap_default \ ivanfranchin/simple-service:1.0.0
-
Open a new terminal
-
Create an environment variable that contains the
Client Secret
generated byKeycloak
tosimple-service
at Configure Keycloak stepSIMPLE_SERVICE_CLIENT_SECRET=...
-
Run the commands below to get an access token for
bgates
userBGATES_TOKEN=$( docker run -t --rm --network springboot-keycloak-openldap_default alpine/curl:latest sh -c ' curl -s -X POST http://keycloak:8080/realms/company-services/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=bgates" \ -d "password=123" \ -d "grant_type=password" \ -d "client_secret='$SIMPLE_SERVICE_CLIENT_SECRET'" \ -d "client_id=simple-service"') BGATES_ACCESS_TOKEN=$(echo $BGATES_TOKEN | jq -r .access_token)
-
Call the endpoint
GET /api/private
curl -i http://localhost:9080/api/private -H "Authorization: Bearer $BGATES_ACCESS_TOKEN"
Shutdown
- To stop
simple-service
application, go to the terminal where it is running and pressCtrl+C
- To stop and remove docker-compose containers, network and volumes, go to a terminal and inside
springboot-keycloak-openldap
root folder, run the following commanddocker-compose down -v
Cleanup
To remove the Docker image create by this project, go to a terminal and, inside springboot-keycloak-openldap
root folder, run the following script
./remove-docker-images.sh