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 42a2040e67117..fec49f5bdf193 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 @@ -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) @@ -1086,4 +1086,12 @@ public static enum ApplicationType { */ HYBRID } + + public ApplicationType getApplicationType() { + return applicationType; + } + + public void setApplicationType(ApplicationType type) { + this.applicationType = type; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index c2a47d3416208..b4409942dc2eb 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -19,7 +19,7 @@ public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMec public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - String token = extractBearerToken(context, resolver.resolve(context, false).oidcConfig); + String token = extractBearerToken(context, resolver.resolveConfig(context)); // if a bearer token is provided try to authenticate if (token != null) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index ba63aa5e4b4cd..eb648935f2ef6 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -86,74 +86,97 @@ public Uni apply(Permission permission) { public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - Cookie sessionCookie = context.request().getCookie(getSessionCookieName(resolver.resolve(context, false))); + final Cookie sessionCookie = context.request().getCookie(getSessionCookieName(resolver.resolveConfig(context))); // if session already established, try to re-authenticate if (sessionCookie != null) { - TenantConfigContext configContext = resolver.resolve(context, true); + Uni resolvedContext = resolver.resolveContext(context); + return resolvedContext.onItem() + .transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantContext) { + return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext); + } + }); + } - AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, - sessionCookie.getValue()); + final String code = context.request().getParam("code"); + if (code == null) { + return Uni.createFrom().optional(Optional.empty()); + } - context.put("access_token", session.getAccessToken()); - return authenticate(identityProviderManager, new IdTokenCredential(session.getIdToken(), context)) - .map(new Function() { - @Override - public SecurityIdentity apply(SecurityIdentity identity) { - if (isLogout(context, configContext)) { - fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); - throw redirectToLogoutEndpoint(context, configContext, session.getIdToken()); - } + // start a new session by starting the code flow dance + Uni resolvedContext = resolver.resolveContext(context); + return resolvedContext.onItem().transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantContext) { + return performCodeFlow(identityProviderManager, context, tenantContext, code); + } + }); + } + + private Uni reAuthenticate(Cookie sessionCookie, + RoutingContext context, + IdentityProviderManager identityProviderManager, + TenantConfigContext configContext) { + AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, + sessionCookie.getValue()); - return augmentIdentity(identity, session.getAccessToken(), session.getRefreshToken(), context); + context.put("access_token", session.getAccessToken()); + return authenticate(identityProviderManager, new IdTokenCredential(session.getIdToken(), context)) + .map(new Function() { + @Override + public SecurityIdentity apply(SecurityIdentity identity) { + if (isLogout(context, configContext)) { + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); + throw redirectToLogoutEndpoint(context, configContext, session.getIdToken()); } - }).on().failure().recoverWithItem(new Function() { - @Override - public SecurityIdentity apply(Throwable throwable) { - if (throwable instanceof AuthenticationRedirectException) { - throw AuthenticationRedirectException.class.cast(throwable); - } - SecurityIdentity identity = null; - - if (!(throwable instanceof TokenAutoRefreshException)) { - Throwable cause = throwable.getCause(); - - if (cause != null && !"expired token".equalsIgnoreCase(cause.getMessage())) { - LOG.debugf("Authentication failure: %s", cause); - throw new AuthenticationCompletionException(cause); - } - if (!configContext.oidcConfig.token.refreshExpired) { - LOG.debug("Token has expired, token refresh is not allowed"); - throw new AuthenticationCompletionException(cause); - } - LOG.debug("Token has expired, trying to refresh it"); - identity = trySilentRefresh(configContext, session.getRefreshToken(), context, - identityProviderManager); - if (identity == null) { - LOG.debug("SecurityIdentity is null after a token refresh"); - throw new AuthenticationCompletionException(); - } else { - fireEvent(SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED, identity); - } + return augmentIdentity(identity, session.getAccessToken(), session.getRefreshToken(), context); + } + }).on().failure().recoverWithItem(new Function() { + @Override + public SecurityIdentity apply(Throwable throwable) { + if (throwable instanceof AuthenticationRedirectException) { + throw AuthenticationRedirectException.class.cast(throwable); + } + + SecurityIdentity identity = null; + + if (!(throwable instanceof TokenAutoRefreshException)) { + Throwable cause = throwable.getCause(); + + if (cause != null && !"expired token".equalsIgnoreCase(cause.getMessage())) { + LOG.debugf("Authentication failure: %s", cause); + throw new AuthenticationCompletionException(cause); + } + if (!configContext.oidcConfig.token.refreshExpired) { + LOG.debug("Token has expired, token refresh is not allowed"); + throw new AuthenticationCompletionException(cause); + } + LOG.debug("Token has expired, trying to refresh it"); + identity = trySilentRefresh(configContext, session.getRefreshToken(), context, + identityProviderManager); + if (identity == null) { + LOG.debug("SecurityIdentity is null after a token refresh"); + throw new AuthenticationCompletionException(); + } else { + fireEvent(SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED, identity); + } + } else { + identity = trySilentRefresh(configContext, session.getRefreshToken(), context, + identityProviderManager); + if (identity == null) { + LOG.debug("ID token can no longer be refreshed, using the current SecurityIdentity"); + identity = ((TokenAutoRefreshException) throwable).getSecurityIdentity(); } else { - identity = trySilentRefresh(configContext, session.getRefreshToken(), context, - identityProviderManager); - if (identity == null) { - LOG.debug("ID token can no longer be refreshed, using the current SecurityIdentity"); - identity = ((TokenAutoRefreshException) throwable).getSecurityIdentity(); - } else { - fireEvent(SecurityEvent.Type.OIDC_SESSION_REFRESHED, identity); - } + fireEvent(SecurityEvent.Type.OIDC_SESSION_REFRESHED, identity); } - return identity; } - }); - } + return identity; + } + }); - // start a new session by starting the code flow dance - context.put("new_authentication", Boolean.TRUE); - return performCodeFlow(identityProviderManager, context, resolver); } private boolean isJavaScript(RoutingContext context) { @@ -174,8 +197,17 @@ private boolean shouldAutoRedirect(TenantConfigContext configContext, RoutingCon public Uni getChallenge(RoutingContext context) { - TenantConfigContext configContext = resolver.resolve(context, true); - removeCookie(context, configContext, getSessionCookieName(configContext)); + Uni tenantContext = resolver.resolveContext(context); + return tenantContext.onItem().transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantContext) { + return getChallengeInternal(context, tenantContext); + } + }); + } + + public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) { + removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); if (!shouldAutoRedirect(configContext, context)) { // If the client (usually an SPA) wants to handle the redirect manually, then @@ -212,14 +244,9 @@ public Uni getChallenge(RoutingContext context) { } private Uni performCodeFlow(IdentityProviderManager identityProviderManager, - RoutingContext context, DefaultTenantConfigResolver resolver) { + RoutingContext context, TenantConfigContext configContext, String code) { - String code = context.request().getParam("code"); - if (code == null) { - return Uni.createFrom().optional(Optional.empty()); - } - - TenantConfigContext configContext = resolver.resolve(context, true); + context.put("new_authentication", Boolean.TRUE); Cookie stateCookie = context.getCookie(getStateCookieName(configContext)); @@ -362,7 +389,7 @@ private void processSuccessfulAuthentication(RoutingContext context, String opaqueAccessToken, String opaqueRefreshToken, SecurityIdentity securityIdentity) { - removeCookie(context, configContext, getSessionCookieName(configContext)); + removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); if (idToken == null) { // it can be null if Vert.x did the remote introspection of the ID token @@ -383,7 +410,7 @@ private void processSuccessfulAuthentication(RoutingContext context, String cookieValue = resolver.getTokenStateManager() .createTokenState(context, configContext.oidcConfig, new AuthorizationCodeTokens(opaqueIdToken, opaqueAccessToken, opaqueRefreshToken)); - createCookie(context, configContext.oidcConfig, getSessionCookieName(configContext), cookieValue, maxAge); + createCookie(context, configContext.oidcConfig, getSessionCookieName(configContext.oidcConfig), cookieValue, maxAge); fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); } @@ -565,7 +592,7 @@ private boolean isForceHttps(TenantConfigContext configContext) { private AuthenticationRedirectException redirectToLogoutEndpoint(RoutingContext context, TenantConfigContext configContext, String idToken) { - removeCookie(context, configContext, getSessionCookieName(configContext)); + removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); return new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context)); } @@ -579,8 +606,8 @@ private static String getPostLogoutCookieName(TenantConfigContext configContext) return POST_LOGOUT_COOKIE_NAME + cookieSuffix; } - private static String getSessionCookieName(TenantConfigContext configContext) { - String cookieSuffix = getCookieSuffix(configContext.oidcConfig.tenantId.get()); + private static String getSessionCookieName(OidcTenantConfig oidcConfig) { + String cookieSuffix = getCookieSuffix(oidcConfig.tenantId.get()); return SESSION_COOKIE_NAME + cookieSuffix; } 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 ee4c9fa6b3e38..b146aadb1f37c 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 @@ -1,8 +1,5 @@ package io.quarkus.oidc.runtime; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; @@ -18,13 +15,17 @@ import io.quarkus.oidc.TenantConfigResolver; import io.quarkus.oidc.TenantResolver; import io.quarkus.oidc.TokenStateManager; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @ApplicationScoped public class DefaultTenantConfigResolver { private static final Logger LOG = Logger.getLogger(DefaultTenantConfigResolver.class); - private static final String CURRENT_TENANT_CONFIG = "io.quarkus.oidc.current.tenant.config"; + private static final String CURRENT_STATIC_TENANT_ID = "static.tenant.id"; + private static final String CURRENT_STATIC_TENANT_ID_NULL = "static.tenant.id.null"; + private static final String CURRENT_DYNAMIC_TENANT_CONFIG = "dynamic.tenant.config"; + private static final String CURRENT_DYNAMIC_TENANT_CONFIG_NULL = "dynamic.tenant.config.null"; @Inject Instance tenantResolver; @@ -32,8 +33,6 @@ public class DefaultTenantConfigResolver { @Inject Instance tenantConfigResolver; - private final Map dynamicTenantsConfig = new ConcurrentHashMap<>(); - @Inject TenantConfigBean tenantConfigBean; @@ -62,33 +61,40 @@ public void verifyResolvers() { } } - /** - * Resolve {@linkplain TenantConfigContext} which contains the tenant configuration and - * the active OIDC connection instance which may be null. - * - * @param context the current request context - * @param create if true then the OIDC connection must be available or established - * for the resolution to be successful - * @return - */ - TenantConfigContext resolve(RoutingContext context, boolean create) { - TenantConfigContext config = getTenantConfigFromConfigResolver(context, create); - - if (config == null) { - config = getTenantConfigFromTenantResolver(context); - } else if (create && config.auth == null && !config.oidcConfig.getPublicKey().isPresent()) { - throw new OIDCException("OIDC IDP connection must be available"); + OidcTenantConfig resolveConfig(RoutingContext context) { + OidcTenantConfig tenantConfig = getDynamicTenantConfig(context); + if (tenantConfig == null) { + TenantConfigContext tenant = getStaticTenantContext(context); + if (tenant != null) { + tenantConfig = tenant.oidcConfig; + } } + return tenantConfig; + } + + Uni resolveContext(RoutingContext context) { + Uni tenantContext = getDynamicTenantContext(context); - return config; + if (tenantContext == null) { + tenantContext = Uni.createFrom().item(getStaticTenantContext(context)); + } + return tenantContext; } - private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext context) { + private TenantConfigContext getStaticTenantContext(RoutingContext context) { String tenantId = null; if (tenantResolver.isResolvable()) { - tenantId = tenantResolver.get().resolve(context); + tenantId = context.get(CURRENT_STATIC_TENANT_ID); + if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) { + tenantId = tenantResolver.get().resolve(context); + if (tenantId != null) { + context.put(CURRENT_STATIC_TENANT_ID, tenantId); + } else { + context.put(CURRENT_STATIC_TENANT_ID_NULL, true); + } + } } TenantConfigContext configContext = tenantId != null ? tenantConfigBean.getStaticTenantsConfig().get(tenantId) : null; @@ -101,13 +107,6 @@ private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext con return configContext; } - boolean isBlocking(RoutingContext context) { - TenantConfigContext resolver = resolve(context, false); - return resolver != null - && (resolver.auth == null || resolver.oidcConfig.token.refreshExpired - || resolver.oidcConfig.authentication.userInfoRequired); - } - boolean isSecurityEventObserved() { return securityEventObserved; } @@ -124,36 +123,34 @@ TokenStateManager getTokenStateManager() { return tokenStateManager.get(); } - private TenantConfigContext getTenantConfigFromConfigResolver(RoutingContext context, boolean create) { + private OidcTenantConfig getDynamicTenantConfig(RoutingContext context) { + OidcTenantConfig oidcConfig = null; if (tenantConfigResolver.isResolvable()) { - OidcTenantConfig tenantConfig; - - if (context.get(CURRENT_TENANT_CONFIG) != null) { - tenantConfig = context.get(CURRENT_TENANT_CONFIG); - } else { - tenantConfig = this.tenantConfigResolver.get().resolve(context); - if (tenantConfig != null) { - context.put(CURRENT_TENANT_CONFIG, tenantConfig); + oidcConfig = context.get(CURRENT_DYNAMIC_TENANT_CONFIG); + if (oidcConfig == null && context.get(CURRENT_DYNAMIC_TENANT_CONFIG_NULL) == null) { + oidcConfig = tenantConfigResolver.get().resolve(context); + if (oidcConfig != null) { + context.put(CURRENT_DYNAMIC_TENANT_CONFIG, oidcConfig); + } else { + context.put(CURRENT_DYNAMIC_TENANT_CONFIG_NULL, true); } } + } + return oidcConfig; + } - if (tenantConfig != null) { - String tenantId = tenantConfig.getTenantId() - .orElseThrow(() -> new OIDCException("Tenant configuration must have tenant id")); - TenantConfigContext tenantContext = dynamicTenantsConfig.get(tenantId); - - if (tenantContext == null) { - if (create) { - synchronized (dynamicTenantsConfig) { - tenantContext = dynamicTenantsConfig.computeIfAbsent(tenantId, - clientId -> tenantConfigBean.getTenantConfigContextFactory().apply(tenantConfig)); - } - } else { - tenantContext = new TenantConfigContext(null, tenantConfig); - } - } + private Uni getDynamicTenantContext(RoutingContext context) { - return tenantContext; + OidcTenantConfig tenantConfig = getDynamicTenantConfig(context); + if (tenantConfig != null) { + String tenantId = tenantConfig.getTenantId() + .orElseThrow(() -> new OIDCException("Tenant configuration must have tenant id")); + TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId); + + if (tenantContext == null) { + return tenantConfigBean.getTenantConfigContextFactory().apply(tenantConfig); + } else { + return Uni.createFrom().item(tenantContext); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JwkSetRefreshHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JwkSetRefreshHandler.java index b317338f233b7..73d1e96d3272c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JwkSetRefreshHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JwkSetRefreshHandler.java @@ -11,7 +11,7 @@ public class JwkSetRefreshHandler implements Handler { private static final Logger LOG = Logger.getLogger(JwkSetRefreshHandler.class); private OAuth2Auth auth; private volatile long lastForcedRefreshTime; - private long forcedJwksRefreshIntervalMilliSecs; + private volatile long forcedJwksRefreshIntervalMilliSecs; public JwkSetRefreshHandler(OAuth2Auth auth, Duration forcedJwksRefreshInterval) { this.auth = auth; 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 80c4dd9b5488a..f3ec0c4282125 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 @@ -37,37 +37,37 @@ public void init() { @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - TenantConfigContext tenantContext = resolve(context); - if (tenantContext.oidcConfig.tenantEnabled == false) { + OidcTenantConfig oidcConfig = resolve(context); + if (oidcConfig.tenantEnabled == false) { return Uni.createFrom().nullItem(); } - return isWebApp(context, tenantContext) ? codeAuth.authenticate(context, identityProviderManager) + return isWebApp(context, oidcConfig) ? codeAuth.authenticate(context, identityProviderManager) : bearerAuth.authenticate(context, identityProviderManager); } @Override public Uni getChallenge(RoutingContext context) { - TenantConfigContext tenantContext = resolve(context); - if (tenantContext.oidcConfig.tenantEnabled == false) { + OidcTenantConfig oidcConfig = resolve(context); + if (oidcConfig.tenantEnabled == false) { return Uni.createFrom().nullItem(); } - return isWebApp(context, tenantContext) ? codeAuth.getChallenge(context) + return isWebApp(context, oidcConfig) ? codeAuth.getChallenge(context) : bearerAuth.getChallenge(context); } - private TenantConfigContext resolve(RoutingContext context) { - TenantConfigContext tenantContext = resolver.resolve(context, false); - if (tenantContext == null) { - throw new OIDCException("Tenant configuration context has not been resolved"); + private OidcTenantConfig resolve(RoutingContext context) { + OidcTenantConfig oidcConfig = resolver.resolveConfig(context); + if (oidcConfig == null) { + throw new OIDCException("Tenant configuration has not been resolved"); } - return tenantContext; + return oidcConfig; } - private boolean isWebApp(RoutingContext context, TenantConfigContext tenantContext) { - if (OidcTenantConfig.ApplicationType.HYBRID == tenantContext.oidcConfig.applicationType) { + private boolean isWebApp(RoutingContext context, OidcTenantConfig oidcConfig) { + if (OidcTenantConfig.ApplicationType.HYBRID == oidcConfig.applicationType) { return context.request().getHeader("Authorization") == null; } - return OidcTenantConfig.ApplicationType.WEB_APP == tenantContext.oidcConfig.applicationType; + return OidcTenantConfig.ApplicationType.WEB_APP == oidcConfig.applicationType; } @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 c753e191a9dc2..7112becb340a3 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 @@ -4,6 +4,7 @@ import java.security.Principal; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import javax.enterprise.context.ApplicationScoped; @@ -48,28 +49,35 @@ public Uni authenticate(TokenAuthenticationRequest request, OidcTokenCredential credential = (OidcTokenCredential) request.getToken(); RoutingContext vertxContext = credential.getRoutingContext(); vertxContext.put(AuthenticationRequestContext.class.getName(), context); - return Uni.createFrom().deferred(new Supplier>() { - @Override - public Uni get() { - if (tenantResolver.isBlocking(vertxContext)) { - return context.runBlocking(new Supplier() { - @Override - public SecurityIdentity get() { - return authenticate(request, vertxContext).await().indefinitely(); - } - }); - } - return authenticate(request, vertxContext); - } - }); + Uni tenantConfigContext = tenantResolver.resolveContext(vertxContext); + return tenantConfigContext.onItem() + .transformToUni(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantConfigContext) { + return Uni.createFrom().deferred(new Supplier>() { + @Override + public Uni get() { + if (isTenantBlocking(tenantConfigContext)) { + return context.runBlocking(new Supplier() { + @Override + public SecurityIdentity get() { + return authenticate(request, vertxContext, tenantConfigContext).await() + .indefinitely(); + } + }); + } + return authenticate(request, vertxContext, tenantConfigContext); + } + }); + } + }); } private Uni authenticate(TokenAuthenticationRequest request, - RoutingContext vertxContext) { - TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true); - + RoutingContext vertxContext, + TenantConfigContext resolvedContext) { if (resolvedContext.oidcConfig.publicKey.isPresent()) { return validateTokenWithoutOidcServer(request, resolvedContext); } else { @@ -283,4 +291,8 @@ public void handle(AsyncResult event) { } }).await().indefinitely(); } + + private static boolean isTenantBlocking(TenantConfigContext resolvedContext) { + return resolvedContext.oidcConfig.token.refreshExpired || resolvedContext.oidcConfig.authentication.userInfoRequired; + } } 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 4dcf16ea12292..c9939db7ae1c7 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 @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -18,6 +19,8 @@ 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.BlockingOperationControl; +import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; import io.smallrye.mutiny.Uni; @@ -39,9 +42,11 @@ public class OidcRecorder { private static final Logger LOG = Logger.getLogger(OidcRecorder.class); + private static final Map dynamicTenantsConfig = new ConcurrentHashMap<>(); + public Supplier setup(OidcConfig config, Supplier vertx) { final Vertx vertxValue = vertx.get(); - Map tenantsConfig = new HashMap<>(); + Map staticTenantsConfig = new HashMap<>(); for (Map.Entry tenant : config.namedTenants.entrySet()) { if (config.defaultTenant.getTenantId().isPresent() @@ -52,25 +57,55 @@ public Supplier setup(OidcConfig config, Supplier vertx throw new OIDCException("Configuration has 2 different tenant-id values: '" + tenant.getKey() + "' and '" + tenant.getValue().getTenantId().get() + "'"); } - tenantsConfig.put(tenant.getKey(), createTenantContext(vertxValue, tenant.getValue(), tenant.getKey())); + staticTenantsConfig.put(tenant.getKey(), createTenantContext(vertxValue, tenant.getValue(), tenant.getKey())); } + TenantConfigContext tenantContext = createTenantContext(vertxValue, config.defaultTenant, "Default"); return new Supplier() { @Override public TenantConfigBean get() { - return new TenantConfigBean(tenantsConfig, tenantContext, - new Function() { + return new TenantConfigBean(staticTenantsConfig, dynamicTenantsConfig, tenantContext, + new Function>() { @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()); + public Uni apply(OidcTenantConfig config) { + if (BlockingOperationControl.isBlockingAllowed()) { + try { + return Uni.createFrom().item(createDynamicTenantContext(vertxValue, config, + config.getTenantId().get())); + } catch (Throwable t) { + return Uni.createFrom().failure(t); + } + } else { + return Uni.createFrom().emitter(new Consumer>() { + @Override + public void accept(UniEmitter uniEmitter) { + ExecutorRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + try { + uniEmitter.complete(createDynamicTenantContext(vertxValue, config, + config.getTenantId().get())); + } catch (Throwable t) { + uniEmitter.fail(t); + } + } + }); + } + }); + } } }); } }; } + private TenantConfigContext createDynamicTenantContext(Vertx vertx, OidcTenantConfig oidcConfig, String tenantId) { + if (!dynamicTenantsConfig.containsKey(tenantId)) { + dynamicTenantsConfig.putIfAbsent(tenantId, createTenantContext(vertx, oidcConfig, tenantId)); + } + return dynamicTenantsConfig.get(tenantId); + } + private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oidcConfig, String tenantId) { if (!oidcConfig.tenantId.isPresent()) { oidcConfig.tenantId = Optional.of(tenantId); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java index aab5ca8e46009..47f83de514ec0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java @@ -4,16 +4,22 @@ import java.util.function.Function; import io.quarkus.oidc.OidcTenantConfig; +import io.smallrye.mutiny.Uni; public class TenantConfigBean { private final Map staticTenantsConfig; + private final Map dynamicTenantsConfig; private final TenantConfigContext defaultTenant; - private final Function tenantConfigContextFactory; + private final Function> tenantConfigContextFactory; - public TenantConfigBean(Map staticTenantsConfig, TenantConfigContext defaultTenant, - Function tenantConfigContextFactory) { + public TenantConfigBean( + Map staticTenantsConfig, + Map dynamicTenantsConfig, + TenantConfigContext defaultTenant, + Function> tenantConfigContextFactory) { this.staticTenantsConfig = staticTenantsConfig; + this.dynamicTenantsConfig = dynamicTenantsConfig; this.defaultTenant = defaultTenant; this.tenantConfigContextFactory = tenantConfigContextFactory; } @@ -26,7 +32,11 @@ public TenantConfigContext getDefaultTenant() { return defaultTenant; } - public Function getTenantConfigContextFactory() { + public Function> getTenantConfigContextFactory() { return tenantConfigContextFactory; } + + public Map getDynamicTenantsConfig() { + return dynamicTenantsConfig; + } } 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 41231a0009c57..8cbffc40d82c2 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; @@ -10,6 +12,12 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { @Override public OidcTenantConfig resolve(RoutingContext context) { + // Make sure this resolver is called only once during a given request + if (context.get("dynamic_config_resolved") != null) { + throw new RuntimeException(); + } + context.put("dynamic_config_resolved", "true"); + String path = context.request().path(); String tenantId = path.split("/")[2]; if ("tenant-d".equals(tenantId)) { @@ -41,6 +49,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/CustomTenantResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 0fbbb2c50700e..0d6947562f813 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -10,6 +10,12 @@ public class CustomTenantResolver implements TenantResolver { @Override public String resolve(RoutingContext context) { + // Make sure this resolver is called only once during a given request + if (context.get("static_config_resolved") != null) { + throw new RuntimeException(); + } + context.put("static_config_resolved", "true"); + if (context.request().path().endsWith("/tenant-public-key")) { return "tenant-public-key"; } 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 02d7005303960..1b352f6da737c 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 123bc0e811ea2..d42ec6909b997 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 @@ -316,6 +316,23 @@ public void testSimpleOidcNoDiscovery() { RestAssured.when().get("/oidc/jwk-endpoint-call-count").then().body(equalTo("1")); } + @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(); + } + } + private String getAccessToken(String userName, String clientId) { return RestAssured .given()