Repository: quarkusio/quarkus
Issue: oidc use Programmatically Resolving Tenants Configuration BUT hangs

Hi
@sberyozkin
@pedroigor

regarding to this: 
https://quarkus.io/guides/security-openid-connect-multitenancy#programmatically-resolving-tenants-configuration

if i used configuration ways, it works fine and can redirect to keycloak,

BUT when uses programmatically way it hangs there and keep waiting.

//////////////////////////
I use intellj debug mode and dive into the source code, I found the issue is in this part,

in DefaultTenantConfigResolver.java

if (tenantContext == null) {
if (create) {
synchronized (dynamicTenantsConfig) {
tenantContext = dynamicTenantsConfig.computeIfAbsent(tenantId,
clientId -> tenantConfigBean.getTenantConfigContextFactory().apply(tenantConfig));
}
} else {
tenantContext = new TenantConfigContext(null, tenantConfig);
}
}
=======>>>>>

synchronized (dynamicTenantsConfig) {
tenantContext = dynamicTenantsConfig.computeIfAbsent(tenantId,
clientId -> tenantConfigBean.getTenantConfigContextFactory().apply(tenantConfig));
}

this one looks the root issue cause,
when reach this line, it terminated.

can you please have a look?

BTW here is show that
I used the programmtical way this is my application code level:

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

@Override
public OidcTenantConfig resolve(RoutingContext context) {
    String path = context.request().path();
    String[] parts = path.split("/");

    if (parts.length == 0) {
        // resolve to default tenant configuration
        return null;
    }

    if ("tenant-a".equals(parts[1])) {
        OidcTenantConfig config = new OidcTenantConfig();
         config.setTenantId("tenant-a");
        config.setAuthServerUrl("http://localhost:8180/auth/realms/tenant-a");
        config.setClientId("multi-tenant-client");
        config.applicationType = OidcTenantConfig.ApplicationType.WEB_APP;
        return config;
    }

    // resolve to default tenant configuration
    return null;
}
}



can you please have a look, thanks!

Changed files:
integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java
integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java
integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java
integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTenantConfigResolver.java
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java

diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java
index 13481c193a6..97efb735350 100644
--- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java
+++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerRecorder.java
@@ -12,8 +12,9 @@ public class KeycloakPolicyEnforcerRecorder {
 
     public void setup(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config, BeanContainer beanContainer,
             HttpConfiguration httpConfiguration) {
-        if (oidcConfig.defaultTenant.applicationType == OidcTenantConfig.ApplicationType.WEB_APP) {
-            throw new OIDCException("Application type [" + oidcConfig.defaultTenant.applicationType + "] is not supported");
+        if (oidcConfig.defaultTenant.getApplicationType() == OidcTenantConfig.ApplicationType.WEB_APP) {
+            throw new OIDCException(
+                    "Application type [" + oidcConfig.defaultTenant.getApplicationType() + "] is not supported");
         }
         beanContainer.instance(KeycloakPolicyEnforcerAuthorizer.class).init(oidcConfig, config, httpConfiguration);
     }
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTenantConfigResolver.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTenantConfigResolver.java
index 35a6d8d85c6..4d654d95b75 100644
--- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTenantConfigResolver.java
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTenantConfigResolver.java
@@ -16,7 +16,7 @@ public OidcTenantConfig resolve(RoutingContext context) {
             config.setTenantId("tenant-config-resolver");
             config.setAuthServerUrl(getIssuerUrl() + "/realms/devmode");
             config.setClientId("client-dev-mode");
-            config.applicationType = ApplicationType.WEB_APP;
+            config.setApplicationType(ApplicationType.WEB_APP);
             return config;
         }
         return null;
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
index 42a2040e671..bee328b75df 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
@@ -187,8 +187,8 @@ public enum Verification {
          * Certificate validation and hostname verification, which can be one of the following values from enum
          * {@link Verification}. Default is required.
          */
-        @ConfigItem(defaultValue = "REQUIRED")
-        public Verification verification;
+        @ConfigItem(defaultValue = "required")
+        public Verification verification = Verification.REQUIRED;
 
         public Verification getVerification() {
             return verification;
@@ -257,7 +257,7 @@ public enum Strategy {
          * Default TokenStateManager strategy.
          */
         @ConfigItem(defaultValue = "keep_all_tokens")
-        public Strategy strategy;
+        public Strategy strategy = Strategy.KEEP_ALL_TOKENS;
 
         /**
          * Default TokenStateManager keeps all tokens (ID, access and refresh)
@@ -437,6 +437,14 @@ public Logout getLogout() {
         return logout;
     }
 
+    public ApplicationType getApplicationType() {
+        return applicationType;
+    }
+
+    public void setApplicationType(ApplicationType applicationType) {
+        this.applicationType = applicationType;
+    }
+
     @ConfigGroup
     public static class Credentials {
 
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java
index 635d42b9224..344180afdf0 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java
@@ -99,8 +99,7 @@ private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext con
     boolean isBlocking(RoutingContext context) {
         TenantConfigContext resolver = resolve(context, false);
         return resolver != null
-                && (resolver.auth == null || resolver.oidcConfig.token.refreshExpired
-                        || resolver.oidcConfig.authentication.userInfoRequired);
+                && (resolver.oidcConfig.token.refreshExpired || resolver.oidcConfig.authentication.userInfoRequired);
     }
 
     boolean isSecurityEventObserved() {
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java
index 80c4dd9b548..c0567851b92 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java
@@ -64,10 +64,10 @@ private TenantConfigContext resolve(RoutingContext context) {
     }
 
     private boolean isWebApp(RoutingContext context, TenantConfigContext tenantContext) {
-        if (OidcTenantConfig.ApplicationType.HYBRID == tenantContext.oidcConfig.applicationType) {
+        if (OidcTenantConfig.ApplicationType.HYBRID == tenantContext.oidcConfig.getApplicationType()) {
             return context.request().getHeader("Authorization") == null;
         }
-        return OidcTenantConfig.ApplicationType.WEB_APP == tenantContext.oidcConfig.applicationType;
+        return OidcTenantConfig.ApplicationType.WEB_APP == tenantContext.oidcConfig.getApplicationType();
     }
 
     @Override
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
index c753e191a9d..f383c4fffc0 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
@@ -48,6 +48,8 @@ public Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
         OidcTokenCredential credential = (OidcTokenCredential) request.getToken();
         RoutingContext vertxContext = credential.getRoutingContext();
         vertxContext.put(AuthenticationRequestContext.class.getName(), context);
+        TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true);
+
         return Uni.createFrom().deferred(new Supplier<Uni<? extends SecurityIdentity>>() {
             @Override
             public Uni<SecurityIdentity> get() {
@@ -55,20 +57,19 @@ public Uni<SecurityIdentity> get() {
                     return context.runBlocking(new Supplier<SecurityIdentity>() {
                         @Override
                         public SecurityIdentity get() {
-                            return authenticate(request, vertxContext).await().indefinitely();
+                            return authenticate(request, vertxContext, resolvedContext).await().indefinitely();
                         }
                     });
                 }
 
-                return authenticate(request, vertxContext);
+                return authenticate(request, vertxContext, resolvedContext);
             }
         });
 
     }
 
     private Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
-            RoutingContext vertxContext) {
-        TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true);
+            RoutingContext vertxContext, TenantConfigContext resolvedContext) {
 
         if (resolvedContext.oidcConfig.publicKey.isPresent()) {
             return validateTokenWithoutOidcServer(request, resolvedContext);
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
index 4dcf16ea122..dc380cdeda6 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
@@ -18,6 +18,7 @@
 import io.quarkus.oidc.OidcTenantConfig.Credentials.Secret;
 import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
 import io.quarkus.oidc.OidcTenantConfig.Tls.Verification;
+import io.quarkus.runtime.ExecutorRecorder;
 import io.quarkus.runtime.annotations.Recorder;
 import io.quarkus.runtime.configuration.ConfigurationException;
 import io.smallrye.mutiny.Uni;
@@ -62,9 +63,22 @@ public TenantConfigBean get() {
                         new Function<OidcTenantConfig, TenantConfigContext>() {
                             @Override
                             public TenantConfigContext apply(OidcTenantConfig config) {
-                                // OidcTenantConfig resolved by TenantConfigResolver must have its optional tenantId
-                                // initialized which is also enforced by DefaultTenantConfigResolver
-                                return createTenantContext(vertxValue, config, config.getTenantId().get());
+                                return Uni.createFrom().emitter(new Consumer<UniEmitter<? super TenantConfigContext>>() {
+                                    @Override
+                                    public void accept(UniEmitter<? super TenantConfigContext> uniEmitter) {
+                                        ExecutorRecorder.getCurrent().execute(new Runnable() {
+                                            @Override
+                                            public void run() {
+                                                try {
+                                                    uniEmitter.complete(createTenantContext(vertxValue, config,
+                                                            config.getTenantId().get()));
+                                                } catch (Throwable t) {
+                                                    uniEmitter.fail(t);
+                                                }
+                                            }
+                                        });
+                                    }
+                                }).await().indefinitely();
                             }
                         });
             }
@@ -120,7 +134,7 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
         options.setSite(authServerUrl);
 
         if (!oidcConfig.discoveryEnabled) {
-            if (oidcConfig.applicationType != ApplicationType.SERVICE) {
+            if (oidcConfig.getApplicationType() != ApplicationType.SERVICE) {
                 if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
                     throw new OIDCException("'web-app' applications must have 'authorization-path' and 'token-path' properties "
                             + "set when the discovery is disabled.");
@@ -165,7 +179,7 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
                     "Use only 'credentials.secret' or 'credentials.client-secret' or 'credentials.jwt.secret' property");
         }
 
-        if (ApplicationType.SERVICE.equals(oidcConfig.applicationType)) {
+        if (ApplicationType.SERVICE.equals(oidcConfig.getApplicationType())) {
             if (oidcConfig.token.refreshExpired) {
                 throw new RuntimeException(
                         "The 'token.refresh-expired' property can only be enabled for " + ApplicationType.WEB_APP
@@ -299,7 +313,7 @@ public void accept(UniEmitter<? super OAuth2Auth> uniEmitter) {
     @SuppressWarnings("deprecation")
     private static TenantConfigContext createdTenantContextFromPublicKey(OAuth2ClientOptions options,
             OidcTenantConfig oidcConfig) {
-        if (oidcConfig.applicationType != ApplicationType.SERVICE) {
+        if (oidcConfig.getApplicationType() != ApplicationType.SERVICE) {
             throw new ConfigurationException("'public-key' property can only be used with the 'service' applications");
         }
         LOG.debug("'public-key' property for the local token verification is set,"
diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
index 41231a0009c..fc93034bd4c 100644
--- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
+++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
@@ -3,6 +3,8 @@
 import javax.enterprise.context.ApplicationScoped;
 
 import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
+import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
 import io.quarkus.oidc.TenantConfigResolver;
 import io.vertx.ext.web.RoutingContext;
 
@@ -41,6 +43,16 @@ public OidcTenantConfig resolve(RoutingContext context) {
             config.setJwksPath("jwks");
             config.setClientId("client");
             return config;
+        } else if ("tenant-web-app-dynamic".equals(tenantId)) {
+            OidcTenantConfig config = new OidcTenantConfig();
+            config.setTenantId("tenant-web-app-dynamic");
+            config.setAuthServerUrl(getIssuerUrl() + "/realms/quarkus-webapp");
+            config.setClientId("quarkus-app-webapp");
+            config.getCredentials().setSecret("secret");
+            config.getAuthentication().setUserInfoRequired(true);
+            config.getRoles().setSource(Source.userinfo);
+            config.setApplicationType(ApplicationType.WEB_APP);
+            return config;
         }
         return null;
     }
diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java
index 02d70053039..86ba9be712b 100644
--- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java
+++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java
@@ -56,7 +56,8 @@ public String userNameServiceNoDiscovery(@PathParam("tenant") String tenant) {
     @Path("webapp")
     @RolesAllowed("user")
     public String userNameWebApp(@PathParam("tenant") String tenant) {
-        if (!tenant.equals("tenant-web-app") && !tenant.equals("tenant-web-app-no-discovery")) {
+        if (!tenant.equals("tenant-web-app")
+                && !tenant.equals("tenant-web-app-dynamic") && !tenant.equals("tenant-web-app-no-discovery")) {
             throw new OIDCException("Wrong tenant");
         }
         UserInfo userInfo = getUserInfo();
diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java
index 123bc0e811e..1fde80f8868 100644
--- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java
+++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java
@@ -53,12 +53,29 @@ public void testResolveTenantIdentifierWebApp() throws IOException {
         }
     }
 
+    @Test
+    public void testResolveTenantIdentifierWebAppDynamic() throws IOException {
+        try (final WebClient webClient = createWebClient()) {
+            HtmlPage page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app-dynamic/api/user/webapp");
+            // State cookie is available but there must be no saved path parameter
+            // as the tenant-web-app-dynamic configuration does not set a redirect-path property
+            assertNull(getStateCookieSavedPath(webClient, "tenant-web-app-dynamic"));
+            assertEquals("Log in to quarkus-webapp", page.getTitleText());
+            HtmlForm loginForm = page.getForms().get(0);
+            loginForm.getInputByName("username").setValueAttribute("alice");
+            loginForm.getInputByName("password").setValueAttribute("alice");
+            page = loginForm.getInputByName("login").click();
+            assertEquals("tenant-web-app-dynamic:alice", page.getBody().asText());
+            webClient.getCookieManager().clearCookies();
+        }
+    }
+
     @Test
     public void testResolveTenantIdentifierWebApp2() throws IOException {
         try (final WebClient webClient = createWebClient()) {
             HtmlPage page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app2/api/user/webapp2");
             // State cookie is available but there must be no saved path parameter
-            // as the tenant-web-app configuration does not set a redirect-path property
+            // as the tenant-web-app2 configuration does not set a redirect-path property
             assertNull(getStateCookieSavedPath(webClient, "tenant-web-app2"));
             assertEquals("Log in to quarkus-webapp2", page.getTitleText());
             HtmlForm loginForm = page.getForms().get(0);
diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java
index 6cd30e10faf..e61fee5586e 100644
--- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java
+++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java
@@ -80,6 +80,8 @@ private static RealmRepresentation createRealm(String name) {
         realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false));
         realm.getRoles().getRealm().add(new RoleRepresentation("confidential", null, false));
 
+        realm.setAccessTokenLifespan(10);
+
         return realm;
     }