Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access control with Keycloak #39

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,16 @@ services:
networks:
- demo

keycloak:
container_name: keycloak
image: jboss/keycloak:latest
ports:
- "8180:8080"
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
networks:
- demo

networks:
demo:
Binary file added documentation/Keycloak/keycloak_add_user.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
135 changes: 135 additions & 0 deletions documentation/Keycloak/keycloak_authentication.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
:toc: macro
toc::[]

= Authentication and authorization

Authentication and authorization are important aspects to consider when implementing microservice architectures. A brief overview of these two aspects can be found in the devonfw solutions browser or in the devon4j documentation:

* link:https://github.com/devonfw/solutions/blob/master/solutions/security_authentication/index.asciidoc[Authentication]
* link:https://github.com/devonfw/solutions/blob/master/solutions/security_authorization/index.asciidoc[Authorization]
* link:https://github.com/devonfw/devon4j/blob/master/documentation/guide-access-control.asciidoc[devon4j documentation]

This guide shows how to enable role-based access control using JWTs (link:https://github.com/devonfw/devon4j/blob/master/documentation/guide-jwt.asciidoc[JSON Web Tokens]) and authentication using Keycloak as centralized IAM (Identity and Access Management) solution.

== Setup and configure Keycloak

The `docker-compose.yaml` file already includes a configuration for the setup of Keycloak. Use the `docker-compose up` command to deploy Keycloak in your Docker environment.
You can also install Keycloak on your local machine without Docker. For this, follow the instructions on the official link:https://www.keycloak.org/docs/latest/server_installation/index.html[Keycloak website].

You can access Keycloak by navigating to http://localhost:8180/auth/ in your browser. Open the Administration console and log in with the default username "admin" and password "admin".

The first step is to create a new realm. A realm in Keycloak is used to manage a set of users with their credentials, groups and roles. Click on "Add Realm" (displayed when you hover over "Master" in the upper left corner) and enter "devon4quarkus-product" as the realm name.

Then create a new client named "devon4quarkus-product-cli" with "openid-connect" as the client protocol. We will use this client later to request the access token.

image::keycloak_client.png[Add a new client]

In the next step, add a new user. Click on the "Users" section in the outline, then on "Add user" and fill in the form fields.

image::keycloak_add_user.png[Add a new user]

After this, open the "Credentials" tab and enter the credentials of the user. The newly created user is now able to authenticate himself in Keycloak. By default, new users must enter a new password themselves the first time they log in to fully set up the new account. So open http://localhost:8180/auth/realms/devon4quarkus-product/account, sign in with the new user and enter new credentials for him.

== Enable the access control in Quarkus

To secure the Quarkus application, we use role-based access control (RBAC) using JWTs, which is the common approach in devon4j. We use the Smallrye JWT extension to implement it. For more documentation, see the official link:https://quarkus.io/guides/security-jwt[Quarkus guide on JWT RBAC].

.**Smallrye JWT extension**
[source,xml]
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
----

The extension is already included in the dependencies. To enable it, set `quarkus.smallrye-jwt.enabled=true` in the `application.properties` file.

Additionally, the issuer of the JWT and the location of the public key must be configured. To do this, the following two properties must be set:

[source,properties]
----
mp.jwt.verify.issuer=http://localhost:8180/auth/realms/devon4quarkus-product
mp.jwt.verify.publickey.location=http://localhost:8180/auth/realms/devon4quarkus-product/protocol/openid-connect/certs
----

== Test the application

Now test whether the access control work. Run the application with `mvn clean compile quarkus:dev`.

First, try to make a request to get all products:

.*Bash command*
[source, bash]
----
curl -X GET http://localhost:8080/products -v
----

.*Powershell command*
[source, powershell]
----
Invoke-WebRequest -method GET http://localhost:8080/products
----

You should receive a response with status code 200 containing a list of all products. This is because the corresponding method in the `ProductRestService.java` file is annotated with `@PermitAll`, which causes anyone (even without having a specific role) to be able to access this method.

In the next step, try to delete a product:

.*Bash command*
[source, bash]
----
curl -X DELETE http://localhost:8080/products/100 -v
----

.*Powershell command*
[source, powershell]
----
Invoke-WebRequest -method DELETE http://localhost:8080/products/100
----

You will get an `401 Unauthorized` response. This is because the corresponding method is annotated with `@RolesAllowed(ApplicationAccessControlConfig.PERMISSION_DELETE_PRODUCT)`, which means that only users with a valid token and the corresponding role can access this method.

---

So, in the next step, add the appropriate role to the user. Log in to Keycloak again with the admin credentials, create a new role `devon4quarkus-product.DeleteProduct`, edit the previously created user and add the role to the field "Assigned Roles".

image::keycloak_user_role.png[Configure the role]

After that, you need to obtain a token from Keycloak that you can pass in the request. To do this, use the following commands:

.*Bash command*
[source, bash]
----
curl -d 'client_id=devon4quarkus-product-cli' -d 'username=john doe' -d 'password=demo' -d 'grant_type=password' http://localhost:8180/auth/realms/devon4quarkus-product/protocol/openid-connect/token
----

.*Powershell command*
[source, powershell]
----
$body = @{
"client_id" = "devon4quarkus-product-cli"
"grant_type" = "password"
"username" = "john doe"
"password" = "demo"
}
Invoke-WebRequest -method POST -body $body -contenttype "application/x-www-form-urlencoded" http://localhost:8180/auth/realms/devon4quarkus-product/protocol/openid-connect/token | Select-Object -Expand content
----

Finally, we can test the delete method again. Pass the "access_token" from the previous response as Bearer token in the Authorization header of the request.

.*Bash command*
[source, bash]
----
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:8080/products/100 -v
----

.*Powershell command*
[source, powershell]
----
$headers = @{
Authorization="Bearer $TOKEN"
}
Invoke-WebRequest -method DELETE -headers $headers http://localhost:8080/products/100
----

Now you should be able to delete the product.
Binary file added documentation/Keycloak/keycloak_client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added documentation/Keycloak/keycloak_user_role.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
Expand Down Expand Up @@ -137,7 +140,7 @@
<scope>provided</scope>
</dependency>



<!--Testing -->
<dependency>
Expand All @@ -151,6 +154,11 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand All @@ -165,7 +173,7 @@
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devonfw.quarkus.general.rest.security;

public class ApplicationAccessControlConfig {

public static final String APP_ID = "devon4quarkus-product";

private static final String PREFIX = APP_ID + ".";

public static final String PERMISSION_FIND_PRODUCT = PREFIX + "FindProduct";

public static final String PERMISSION_SAVE_PRODUCT = PREFIX + "SaveProduct";

public static final String PERMISSION_DELETE_PRODUCT = PREFIX + "DeleteProduct";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import java.util.Optional;

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
Expand All @@ -25,6 +27,7 @@
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.springframework.data.domain.Page;

import com.devonfw.quarkus.general.rest.security.ApplicationAccessControlConfig;
import com.devonfw.quarkus.productmanagement.domain.model.ProductEntity;
import com.devonfw.quarkus.productmanagement.domain.repo.ProductRepository;
import com.devonfw.quarkus.productmanagement.rest.v1.mapper.ProductMapper;
Expand All @@ -46,6 +49,7 @@ public class ProductRestService {
UriInfo uriInfo;

@GET
@PermitAll
public Page<ProductDto> getAllOrderedByTitle() {

Page<ProductEntity> products = this.productRepository.findAllByOrderByTitle();
Expand All @@ -56,6 +60,7 @@ public Page<ProductDto> getAllOrderedByTitle() {
}

@POST
@RolesAllowed(ApplicationAccessControlConfig.PERMISSION_SAVE_PRODUCT)
public Response createNewProduct(ProductDto product) {

if (isEmpty(product.getTitle())) {
Expand All @@ -80,6 +85,7 @@ public Page<ProductDto> findProducts(ProductSearchCriteriaDto searchCriteria) {
}

@GET
@PermitAll
@Path("{id}")
public ProductDto getProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {

Expand All @@ -91,13 +97,15 @@ public ProductDto getProductById(@Parameter(description = "Product unique id") @
}

@GET
@PermitAll
@Path("title/{title}")
public ProductDto getProductByTitle(@PathParam("title") String title) {

return this.productMapper.map(this.productRepository.findByTitle(title));
}

@DELETE
@RolesAllowed(ApplicationAccessControlConfig.PERMISSION_DELETE_PRODUCT)
@Path("{id}")
public Response deleteProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {

Expand Down
8 changes: 7 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ quarkus.micrometer.binder.http-server.enabled=true
%test.quarkus.flyway.clean-at-start=true


ryuk.container.image=testcontainersofficial/ryuk
ryuk.container.image=testcontainersofficial/ryuk

#access control settings
#to enable role-based access control based on JWTs, see the keycloak_authentication guide in the documentation/Keycloak folder
quarkus.smallrye-jwt.enabled=false
#mp.jwt.verify.issuer=http://localhost:8180/auth/realms/devon4quarkus-product
#mp.jwt.verify.publickey.location=http://localhost:8180/auth/realms/devon4quarkus-product/protocol/openid-connect/certs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import com.devonfw.quarkus.general.rest.security.ApplicationAccessControlConfig;
import com.devonfw.quarkus.productmanagement.rest.v1.model.ProductDto;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.response.Response;

@QuarkusTest
Expand All @@ -43,6 +45,7 @@ void getNonExistingTest() {
}

@Test
@TestSecurity(user = "testuser", roles = { ApplicationAccessControlConfig.PERMISSION_SAVE_PRODUCT })
@Order(3)
void createNewProduct() {

Expand All @@ -68,6 +71,7 @@ public void testGetById() {
}

@Test
@TestSecurity(user = "testuser", roles = { ApplicationAccessControlConfig.PERMISSION_DELETE_PRODUCT })
@Order(5)
public void deleteById() {

Expand Down