5.10.5. Tenant resolution for OIDC web-app applications
Tenant resolution for the OIDC web-app applications must be done at least 3 times during an authorization code flow, when the OIDC tenant-specific configuration affects how each of the following steps is run.
Step 1: Unauthenticated user accesses an endpoint and is redirected to OIDC provider
When an unauthenticated user accesses a secured path, the user is redirected to the OIDC provider to authenticate and the tenant configuration is used to build the redirect URI.
All the static and dynamic tenant resolution options listed in the Static tenant configuration resolution and Dynamic tenant configuration resolution sections can be used to resolve a tenant.
Step 2: The user is redirected back to the endpoint
After the provider authentication, the user is redirected back to the Quarkus endpoint and the tenant configuration is used to complete the authorization code flow.
All the static and dynamic tenant resolution options listed in the Static tenant configuration resolution and Dynamic tenant configuration resolution sections can be used to resolve a tenant. Before the tenant resolution begins, the authorization code flow state cookie is used to set the already resolved tenant configuration id as a RoutingContext tenant-id attribute: both custom dynamic TenantConfigResolver and static TenantResolver tenant resolvers can check it.
Step 3: Authenticated user accesses the secured path using the session cookie
The tenant configuration determines how the session cookie is verified and refreshed. Before the tenant resolution begins, the authorization code flow session cookie is used to set the already resolved tenant configuration id as a RoutingContext tenant-id attribute: both custom dynamic TenantConfigResolver and static TenantResolver tenant resolvers can check it.
For example, here is how a custom TenantConfigResolver can avoid creating the already resolved tenant configuration, that may otherwise require blocking reads from the database or other remote sources:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
if (resolvedTenantId != null) {
return null;
}
String path = context.request().path();
if (path.endsWith("tenant-a")) {
return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
} else if (path.endsWith("tenant-b")) {
return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
}
// Default tenant id
return null;
}
private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
final OidcTenantConfig config = OidcTenantConfig
.authServerUrl("http://localhost:8180/realms/" + tenantId)
.tenantId(tenantId)
.clientId(clientId)
.credentials(secret)
.applicationType(ApplicationType.WEB_APP)
.build();
return config;
}
}
The default configuration may look like this:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/default
quarkus.oidc.client-id=client-default
quarkus.oidc.credentials.secret=secret-default
quarkus.oidc.application-type=web-app
The preceding example assumes that the tenant-a, tenant-b and default tenants are all used to protect the same endpoint paths. In other words, after the user has authenticated with the tenant-a configuration, this user will not be able to choose to authenticate with the tenant-b or default configuration before this user logs out and has a session cookie cleared or expired.
The situation where multiple OIDC web-app tenants protect the tenant-specific paths is less typical and also requires an extra care. When multiple OIDC web-app tenants such as tenant-a, tenant-b and default tenants are used to control access to the tenant specific paths, the users authenticated with one OIDC provider must not be able to access the paths requiring an authentication with another provider, otherwise the results can be unpredictable, most likely causing unexpected authentication failures. For example, if the tenant-a authentication requires a Keycloak authentication and the tenant-b authentication requires an Auth0 authentication, then, if the tenant-a authenticated user attempts to access a path secured by the tenant-b configuration, then the session cookie will not be verified, since the Auth0 public verification keys can not be used to verify the tokens signed by Keycloak. An easy, recommended way to avoid multiple web-app tenants conflicting with each other is to set the tenant specific session path as shown in the following example:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
if (resolvedTenantId != null) {
return null;
}
String path = context.request().path();
if (path.endsWith("tenant-a")) {
return Uni.createFrom().item(createTenantConfig("tenant-a", "/tenant-a", "client-a", "secret-a"));
} else if (path.endsWith("tenant-b")) {
return Uni.createFrom().item(createTenantConfig("tenant-b", "/tenant-b", "client-b", "secret-b"));
}
// Default tenant id
return null;
}
private OidcTenantConfig createTenantConfig(String tenantId, String cookiePath, String clientId, String secret) {
final OidcTenantConfig config = OidcTenantConfig
.authServerUrl("http://localhost:8180/realms/" + tenantId)
.tenantId(tenantId)
.clientId(clientId)
.credentials(secret)
.applicationType(ApplicationType.WEB_APP)
.authentication().cookiePath(cookiePath).end()
.build();
return config;
}
}
The default tenant configuration should be adjusted like this:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/default
quarkus.oidc.client-id=client-default
quarkus.oidc.credentials.secret=secret-default
quarkus.oidc.authentication.cookie-path=/default
quarkus.oidc.application-type=web-app
Having the same session cookie path when multiple OIDC web-app tenants protect the tenant-specific paths is not recommended and should be avoided as it requires even more care from the custom resolvers, for example:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
String path = context.request().path();
if (path.endsWith("tenant-a")) {
String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
if (resolvedTenantId != null) {
if ("tenant-a".equals(resolvedTenantId)) {
return null;
} else {
// Require a "tenant-a" authentication
context.remove(OidcUtils.TENANT_ID_ATTRIBUTE);
}
}
return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
} else if (path.endsWith("tenant-b")) {
String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
if (resolvedTenantId != null) {
if ("tenant-b".equals(resolvedTenantId)) {
return null;
} else {
// Require a "tenant-b" authentication
context.remove(OidcUtils.TENANT_ID_ATTRIBUTE);
}
}
return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
}
// Set default tenant id
context.put(OidcUtils.TENANT_ID_ATTRIBUTE, OidcUtils.DEFAULT_TENANT_ID);
return null;
}
private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
final OidcTenantConfig config = OidcTenantConfig
.authServerUrl("http://localhost:8180/realms/" + tenantId)
.tenantId(tenantId)
.clientId(clientId)
.credentials(secret)
.applicationType(ApplicationType.WEB_APP)
.build();
return config;
}
}
- 1
- Check the request path to create tenant configurations.
- 2 4
- Let Quarkus use the already resolved tenant configuration if the already resolved tenant is expected for the current path.
- 3 5
- Remove the
tenant-idattribute if the already resolved tenant configuration is not expected for the current path. - 6
- Use the default tenant for all other paths. It is equivalent to removing the
tenant-idattribute.