Tenant Resolver Extension

Warp to the code: Quarkus Tenant Resolver

What is it Link to heading

Simple quarkus extension, implementing TenantConfigResolver. It uses passed regex string to extract tenant ID from request parameters.

Task description Link to heading

At some moment got an idea to add multitenancy support for project in development. Core requirements:

  • all tenants are authenticated against one OIDC server
  • each tenant has own realm
  • tenant name is used within business logic, so should be easily accessible in RequestScope
  • amount of tenants was not pre-defined

In common that is rather straightforward task… when you know how the URL will look like. At the moment of starting implementation of multitenancy support there was no yet agreement on how the URL will look like. Will the tenant information passed as a subdomain? Or specific query parameter? Or path part? It was resolved by setting a technical meeting with involved parties and making decision how it should be. However, it also rised an idea to try to implement something that can be switched with minimal efforts. And that how this project appeared..

Implementation Link to heading

This project is quarkus extension implementation. It provides one main class RegexTenantResolver class that implements TenantConfigResolver. When instantiated it will intercept requests and try to extract tenant information, based on passed parameters. The outcome of interception is creation of custom OidcTenantConfig with set tenant Id, if any is found.

Exact behavior of resolver depends on passed parameters.

Usage Link to heading

Implementation do not define any producer or annotation to auto create bean. Thus to instantiate it - create a producer like:

@ApplicationScoped
public class RegexTenantResolverProducer {

    @ConfigProperty(name = "dev.shebur.tenant-resolver.tenant.source")
    protected TenantSource tenantSource;

    @ConfigProperty(name = "dev.shebur.tenant-resolver.tenant.regex")
    protected String regex;

    @Produces
    @ApplicationScoped
    public RegexTenantResolver regexTenantResolver() {
        return new RegexTenantResolver(
                tenantSource,
                regex
        );
    }

}

…or simply copy code and edit it as needed.

Configuration Link to heading

Parameters that should be passed into resolver:

  • tenantSource - enum value that define what part of request URL should be processed for tenant Id:
    • HOST - tenant is part of host
      https://tenantA.service.my/api/...
      
    • PATH - tenant is part of path
      https://service.my/tenantA/api/...
      
    • QUERY - tenant is passed as query paremeter
      https://service.my/api/?tenant=tenantA
      
    • NONE - there is no tenant Id to be parsed
  • regex - regex string, used to identify how to get tenant information. Resolver expects that first matched group will represent tenant Id

Extending Link to heading

Default implementation will try to extract tenant information and if succeeded will generate a new OidcTenantConfig with extracted tenant Id set. For more advanced use cases it is possible to extend the default implementation. There are 2 main points for overriding:

  • createOidcTenantConfig - place where OidcTenantConfig is created, thus it can be overwritten to make more customizations to OIDC config. For example, calculate custom auth server URL
  • onTenantParsed - generic callback method, executed when tenant information was successfully parsed.

Example of ExtendedRegexTenantResolver can be found under ExtendedRegexTenantResolver.java. Default implementation is improved to:

  1. Calculate custom auth server URL, where used realm name equals to tenant name
    @Override
    protected Uni<OidcTenantConfig> createOidcTenantConfig(String tenant) {
        if (Strings.isNullOrEmpty(tenant)) {
            return Uni.createFrom().nullItem();
        }

        final OidcTenantConfig config = new OidcTenantConfig();
        config.setAuthServerUrl(oidcUrlTemplate.replace(oidcUrlTenantPlaceholder, tenant));
        config.setTenantId(tenant);

        return Uni.createFrom().item(config);
    }
  1. Put tenant information into RoutingContext, so it can be easily extracted from endpoints implementations
    @Override
    protected void onTenantParsed(RoutingContext context, String tenant) {
        context.put("tenant", tenant);
    }

Regex examples Link to heading

Regex string can be edited and checked using some online tool like regex101

Remember about escaping special characters

  • ^([^&.]*) - extract first part of host before dot. Gets tenantA from tenantA.service.my
  • ^/([^&]*)/ - extract first part of path. Gets tenantA from /tenantA/some/service
  • tenant=([^&]*) - extract value for query parameter named tenant. Gets tenantA from ?o=1&tenant=tenantA&t=2