Implement multi tenancy oidc and hibernate on quarkus

Dorian Maliszewski
4 min readFeb 17, 2020

--

For everyone who tested quarkus and tried to implement multi tenancy on it, you saw that it can be a little tricky to do it. Today, I will show you how to implement multi tenancy on quarkus (1.2.0 Final) with keycloak and filter with hibernate in a few minutes and easily.

Here is the github I use for this demo : https://github.com/DorianMaliszewski/quarkus-multi-tenant

Launch an OIDC server

In my docker-compose file you will show that I use keycloak as OIDC server. You can launch it with docker only :

docker run -p 8085:8080 --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -d keycloak

Create a multi tenant configuration on your OIDC server with test user

If you use a keycloak server like me, just import realm configuration’s in the config/keycloak directory.

It creates 3 realm (default, example, example-b), 2 clients in each (frontend and user-service) and 1 user :

username : dorian

password : dorian

Create a quarkus project

Create a quarkus project with the following command :

mvn io.quarkus:quarkus-maven-plugin:1.2.0.Final:create \
-DprojectGroupId=fr.dorianmaliszewski \
-DprojectArtifactId=user-service \
-Dextensions="oidc, resteasy-jackson, hibernate-orm-panache, jdbc-mysql"

Change the configuration

In your src/main/resources/application.properties put the following :

# Configuration file
# configure your datasource
quarkus.datasource.url = jdbc:mysql://localhost:3306/users
quarkus.datasource.driver = com.mysql.cj.jdbc.Driver
quarkus.datasource.username = root
quarkus.datasource.password = root

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create

quarkus.oidc.auth-server-url=http://localhost:8085/auth/realms/default
quarkus.oidc.client-id=user-service
quarkus.oidc.credentials.secret=f0432a65-0663-42ff-9095-5b0d7baa487f
quarkus.oidc.token.issuer=http://localhost:8085/auth/realms/default
quarkus.oidc.authentication.scopes=email,profile,roles

quarkus.oidc.example.auth-server-url=http://localhost:8085/auth/realms/example
quarkus.oidc.example.client-id=user-service
quarkus.oidc.example.credentials.secret=9fe0e33b-6c0d-4fae-9068-3b1c1b43a953
quarkus.oidc.example.token.issuer=http://localhost:8085/auth/realms/example
quarkus.oidc.example.authentication.scopes=email,profile,roles

quarkus.oidc.example-b.auth-server-url=http://localhost:8085/auth/realms/example-b
quarkus.oidc.example-b.client-id=user-service
quarkus.oidc.example-b.credentials.secret=2236d7f0-a305-4074-9fab-3e3a343f9b5b
quarkus.oidc.example-b.token.issuer=http://localhost:8085/auth/realms/example-b
quarkus.oidc.example-b.authentication.scopes=email,profile,roles

Now let’s code the quarkus tenant resolver

To make quarkus select the good configuration you need to make an ApplicationScoped bean that implement the TenantResolver interface like this :

import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

@Override
public String resolve(RoutingContext routingContext) {
String path = routingContext.request().path();
String[] parts = path.split("/");
if (parts.length == 0) {
return null;
}
return parts[1];
}
}

Source in src/main/java/CustomTenantResolver.java.

Of course, you don’t have to take the tenant name in the path, you can take it in a custom header for example.

In the resolve(RoutingContext routingContext) method if you return null, quarkus will take the default configuration, otherwise if you return a String, it will take the tenant configuration.

Let’s try this !

Get an access token in a realm and put in a variable(replace the realm name and client secret to try with other realm) :

  • default : frontend | 4394f3cf-7418–4815-b0cb-00998fd7265d
curl -X POST http://localhost:8085/auth/realms/default/protocol/openid-connect/token \
--user frontend:4394f3cf-7418-4815-b0cb-00998fd7265d \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=dorian&password=dorian&grant_type=password' | jq --raw-output '.access_token'
  • example : frontend |a306766d-52c2–4980–8218–72370fea62a2
curl -X POST http://localhost:8085/auth/realms/example/protocol/openid-connect/token \
--user frontend:a306766d-52c2-4980-8218-72370fea62a2 \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=dorian&password=dorian&grant_type=password' | jq --raw-output '.access_token'
  • example-b : frontend | 78876dfb-036d-4715-bdc0–229fc1540753
curl -X POST http://localhost:8085/auth/realms/example-b/protocol/openid-connect/token \
--user frontend:78876dfb-036d-4715-bdc0-229fc1540753 \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=dorian&password=dorian&grant_type=password' | jq --raw-output '.access_token'

Exemple of command :

export access_token=$(\
curl -X POST http://localhost:8085/auth/realms/default/protocol/openid-connect/token \
--user frontend:4394f3cf-7418-4815-b0cb-00998fd7265d \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=dorian&password=dorian&grant_type=password' | jq --raw-output '.access_token' \
)

Try to get the user

Launch the quarkus dev server with the following command in the project directory :

./mvnw clean compile quarkus:dev -Dquarkus.http.port=8081

Test if all is good

curl -X GET -v http://localhost:8081/default/users/me \
-H "Authorization: Bearer $access_token"
...< HTTP/1.1 200 OK
< Content-Length: 191
< Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"id":1,"createdAt":1581950710158,"updatedAt":1581950710158,"version":0,"tenant":"http://localhost:8085/auth/realms/default","username":"dorian","uuid":"3bb30cdc-fd45-4cc9-a794-266198aaf996"}%

We get a good response !!

And with another tenant

curl -X GET -v http://localhost:8081/example/users/me \
-H "Authorization: Bearer $access_token"
...< HTTP/1.1 403 Forbidden
< content-length: 0
<
* Connection #0 to host localhost left intact

All is good. Next we will implement hibernate configuration for multy tenancy

Implement multi tenancy on hibernate

In a RequestScoped bean you need to filter by the tenant of the user, in my case I set the tenant with the issuer in the claim but you can take the tenant name in the path or something else.

In my src/main/java/services/UserService.java :

@PostConstruct
public void hibernateFilter() {
Session session = Arc.container().instance(EntityManager.class).get().unwrap(Session.class);
Filter filter = session.enableFilter("tenantFilter");
filter.setParameter("tenant", issuer);
filter.validate();
}

And in your entities put a filter : in my case i put it in my BaseEntity super mapped class : src/main/java/models/BaseEntity.java

package models;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.*;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@MappedSuperclass
// Thats' what you need in your class
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name="tenant", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant = :tenant")
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑
//
public class BaseEntity extends PanacheEntityBase {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@CreationTimestamp
private Date createdAt;

@UpdateTimestamp
private Date updatedAt;

@Version
private int version;

private String tenant;
}

And now by default when your RequestScoped bean is constructed it will filter all entities by default with tenant of the user.

That all folks !

Your multi tenancy service is ready now ! Good jobs ! 😃

Thanks to the quarkus team !

Thank to all the work of the quarkus team. I hope this framework will continue in the same way 👏 👏 👏.

--

--