Implement multi tenancy oidc and hibernate on quarkus
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 👏 👏 👏.