To Show how to use Quarkus keycloak Extension in order to protect REST API Resources in Quarkus and manage the access control to it using KeyCloak as an Authentication And Authorization Server. We'll leverage quarkus Dev Service (and running the quarkus application in DEV Mode off course) to use its automatically provisioned keycloak instance in a container.
This project uses Quarkus, the Supersonic Subatomic Java Framework.
If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
You can run your application in dev mode that enables live coding using:
./mvnw compile quarkus:dev
NOTE: Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/.
A Keycloak instance was provisioned and its details available at http://localhost:8080/q/dev-ui/dev-services
- Enter the keycloak instance written in
keycloak.url
property and concatenate the/admin
url path(e.g http://localhost:36271/admin), and authenticate using the default admin/admin credentials. - Switch To
Quarkus
Realm - Click on
Clients
menu on the left panel - Choose "quarkus-app" Client ID and enter it.
Go to Settings
tab, and go to Capability config
Client authentication= On
Authorization= On
- Standard flow
- Direct access grants
- Implicit flow
- OAuth 2.0 Device Authorization Grant
- OIDC CIBA Grant
Finally Click on save Button
- Go to
Roles
Tab - Click on
Create role
Button - Type in Role name field read_book -> Click on
Save
Button - Repeat Steps 2-3 for write_book role.
- On the left panel, go to users, and click on
Add User
Button. - Write down in username field "bookreader" and click on create button, then go to
Credentials
tab -> Click on "Set password" button" --> fill password and password confirmation, and toggle off Temporary field--> Click on save. - Go to
Role mapping
tab, and click onAssign role
Button -> Filter by clients -> Choose read_bookrole
of "quarkus-app"Client
--> click on assign. - Click again on Users left menu -> click on
Add User
Button. - Write down in username field "bookadmin" and click on create button, then go to
Credentials
tab -> Click on "Set password" button" --> fill password and password confirmation, and toggle off Temporary field--> Click on save. - Go to
Role mapping
tab, and click onAssign role
Button -> Filter by clients -> Choose write_bookrole
of "quarkus-app"Client
--> click on assign.
We're going to protect /api/books
RESTFUL API resource which we defined in this quarkus application.
Go to Clients menu -> Choose "quarkus-app" Client" -> Go to Authorization
Tab.
On Settings
sub-tab (Under Authorization tab), choose the following:
Policy enforcement mode= Enforcing
Decision Strategy= Affirmative
Remote Resource Management= On
- Go to Client --> Choose
Authorization
tab, and go toScopes
sub-tab - Click on button
Create Authorization Scope
- fill in the following:
Name=read
DisplayName=read
Icon URI=
- Click on Save Button.
- Repeat 1-4 in order to create "write" Authorization Scope.
You should see 2 Authorization Scopes:
- Go to Client --> Choose
Authorization
tab, and go toResources
sub-tab - Click on button
Create resource
- Fill the details according to the next screenshot, and Click on Save
- Go to Client --> Choose
Authorization
tab, and go toPolicies
sub-tab - Click on button `Create policy
- Choose Policy type With Name = Role.
- Fill in the details like the following, to create read_book policy, and click on save at the end:
- Repeat Steps 1-4 for creating write_book policy, you should fill the following values (and click on save at the end):
- Go to Client --> Choose
Authorization
tab, and go toPermissions
sub-tab - Click on button
Create Permission
, and in the Drop down list, choose "Create scope-based Permission": - Create Permission "read_book_permission" with the following details, and click save :
- Repeat 1-3 for another permission "write_book_permission" with the following details:
application.properties:
quarkus.keycloak.policy-enforcer.enable=true
# Enables mapping http methods to Scopes, that way we can distinguish between
# different methods/operation on the same rest resource
# and can adhere to REST API best practices
quarkus.keycloak.policy-enforcer.http-method-as-scope=true
quarkus.keycloak.policy-enforcer.paths.1.name=read_book_permission
quarkus.keycloak.policy-enforcer.paths.1.path=/api/books/*
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=ENFORCING
quarkus.keycloak.policy-enforcer.paths.1.methods.get.method=GET
quarkus.keycloak.policy-enforcer.paths.1.methods.get.scopes=read
#
#
quarkus.keycloak.policy-enforcer.paths.2.name=write_book_permission
quarkus.keycloak.policy-enforcer.paths.2.path=/api/books/*
quarkus.keycloak.policy-enforcer.paths.2.enforcement-mode=ENFORCING
quarkus.keycloak.policy-enforcer.paths.2.methods.post.method=POST
quarkus.keycloak.policy-enforcer.paths.2.methods.post.scopes=write
bookResource.java
@ApplicationScoped
@Path("/api/books")
@Authenticated
public class BookResource {
private static final Logger LOG = Logger.getLogger(BookResource.class);
@Inject
SecurityIdentity identity;
@Inject
@Named("InMemoryBooksService")
protected BooksService getBooksService;
@GET
public List<BookModel> getAll() {
return getBooksService.getAllBooks();
}
@GET
@Path("/{id}")
// Don't need @RolesAllowed annotation as Keycloak manages it and the extension getting its decision based on permission and its policies on the resource.
// @RolesAllowed(value = {"write_book", "read_book"})
public BookModel getOne(@PathParam("id") String id) {
return getBooksService.getBook(id);
}
@POST
public Response CreateOne(@RequestBody @Valid BookModel book) {
Response result;
try
{
getBooksService.CreateBook(book);
result = Response.created(URI.create("localhost")).build();
}
catch (Exception e)
{
LOG.errorf("Error Creating book, error message: %s",e.getMessage());
result = Response.serverError().entity("Error creating the book, Check the logs or reach out API's Administrator.").build();
}
return result;
}
}
- Trying to consume the API Without Authentication
curl -I --location --request GET 'http://localhost:8080/api/books'
Output:
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer
content-length: 0
- Authenticate bookReader user using quarkus-app client in order to retrieve from keycloak an access token (change all details according to the passwords you've set on keycloak, and according to assigned listening port of keycloak running in container ):
export ACCESS_TOKEN=$(curl --location --request POST 'http://localhost:36271/realms/quarkus/protocol/openid-connect/token' --header 'content-type: application/x-www-form-urlencoded' --header 'Authorization: Basic cXVhcmt1cy1hcHA6c2VjcmV0' --data-urlencode 'username=bookreader' --data-urlencode 'password=Redhat123#' --data-urlencode 'grant_type=password' | jq .access_token | tr -d '"')
- Now try to consume the API GET Endpoint using the retrieved token:
curl --location --request GET 'http://localhost:8080/api/books' -H 'Authorization: Bearer '$ACCESS_TOKEN'' | jq .
Output:
[
{
"id": "test",
"name": "test-book",
"genre": "Comedy",
"numOfPages": 250,
"authorName": "John Doe",
"price": 60,
"publishingDate": "1984-01-03"
}
]
- Now try to create a Book With the "bookreader" user ( Invoke Rest API POST Method) , using bookreader' valid and not expired token of that user:
curl -i --location --request POST 'http://localhost:8080/api/books' \
--header 'Authorization: Bearer '$ACCESS_TOKEN'' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "demo book",
"name": "demo book test",
"genre": "Science",
"numOfPages": 350,
"authorName": "John Doe",
"price": 25,
"publishingDate": "2019-01-18"
}'
Output:
HTTP/1.1 403 Forbidden
content-length: 0
- Now Authenticate using the "bookadmin" User and get access token from keycloak ( change all details according to the passwords you've set on keycloak, and according to assigned listening port of keycloak running in container):
export ACCESS_TOKEN=$(curl --location --request POST 'http://localhost:36271/realms/quarkus/protocol/openid-connect/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic cXVhcmt1cy1hcHA6c2VjcmV0' \
--data-urlencode 'username=bookadmin' \
--data-urlencode 'password=bookadmin' \
--data-urlencode 'grant_type=password' | jq .access_token | tr -d '"')
- Now try to invoke the POST Method to create a book, using token of the privileged user bookadmin:
curl -i --location --request POST 'http://localhost:8080/api/books' \
--header 'Authorization: Bearer '$ACCESS_TOKEN'' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "demo-book",
"name": "demo book test",
"genre": "Science",
"numOfPages": 350,
"authorName": "John Doe",
"price": 25,
"publishingDate": "2019-01-18"
}'
Output:
HTTP/1.1 201 Created
Location: http://localhost:8080/api/books/demo-book
content-length: 0
-
For your convenience , please find below a postman collection and environment files attached , with all endpoints to authenticate to retrieve tokens , and to Invoke the API endpoints, just change the environment variables( their scope is collection, not global) according to the users' passwords and with the right addresses and ports.
kindly import them to your postman client:
The application can be packaged using:
./mvnw package
It produces the quarkus-run.jar
file in the target/quarkus-app/
directory.
Be aware that it’s not an über-jar as the dependencies are copied into the target/quarkus-app/lib/
directory.
The application is now runnable using java -jar target/quarkus-app/quarkus-run.jar
.
If you want to build an über-jar, execute the following command:
./mvnw package -Dquarkus.package.type=uber-jar
The application, packaged as an über-jar, is now runnable using java -jar target/*-runner.jar
.
You can create a native executable using:
./mvnw package -Pnative
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
./mvnw package -Pnative -Dquarkus.native.container-build=true
You can then execute your native executable with: ./target/code-with-quarkus-1.0.0-SNAPSHOT-runner
If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.
- SmallRye OpenTracing (guide): Trace your services with SmallRye OpenTracing
- Keycloak Authorization (guide): Policy enforcer using Keycloak-managed permissions to control access to protected resources
- Hibernate Validator (guide): Validate object properties (field, getter) and method parameters for your beans (REST, CDI, Jakarta Persistence)
- SmallRye OpenAPI (guide): Document your REST APIs with OpenAPI - comes with Swagger UI
- Camel Kubernetes (guide): Perform operations against Kubernetes API
- OpenTelemetry (guide): Use OpenTelemetry to trace services